mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
2 Commits
wch-startc
...
barret/deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2acaea1444 | ||
|
|
1120cfdfd7 |
26
DESCRIPTION
26
DESCRIPTION
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.5.0.9005
|
||||
Version: 1.5.0.9004
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
|
||||
@@ -80,22 +80,20 @@ Imports:
|
||||
mime (>= 0.3),
|
||||
jsonlite (>= 0.9.16),
|
||||
xtable,
|
||||
digest (>= 0.6.25),
|
||||
digest,
|
||||
htmltools (>= 0.5.0.9001),
|
||||
R6 (>= 2.0),
|
||||
sourcetools,
|
||||
later (>= 1.0.0),
|
||||
promises (>= 1.1.1.9001),
|
||||
promises (>= 1.1.0),
|
||||
tools,
|
||||
crayon,
|
||||
rlang (>= 0.4.8.9002),
|
||||
rlang (>= 0.4.0),
|
||||
fastmap (>= 1.0.0),
|
||||
withr,
|
||||
commonmark (>= 1.7),
|
||||
glue (>= 1.3.2),
|
||||
bslib (>= 0.2.2.9002),
|
||||
cachem,
|
||||
ellipsis
|
||||
bootstraplib (>= 0.2.0.9001)
|
||||
Suggests:
|
||||
datasets,
|
||||
Cairo (>= 1.5-5),
|
||||
@@ -106,7 +104,7 @@ Suggests:
|
||||
ggplot2,
|
||||
reactlog (>= 1.0.0),
|
||||
magrittr,
|
||||
shinytest (>= 1.4.0.9003),
|
||||
shinytest,
|
||||
yaml,
|
||||
future,
|
||||
dygraphs,
|
||||
@@ -115,20 +113,14 @@ Suggests:
|
||||
sass
|
||||
Remotes:
|
||||
rstudio/htmltools,
|
||||
rstudio/promises,
|
||||
rstudio/sass,
|
||||
rstudio/bslib,
|
||||
rstudio/shinytest,
|
||||
r-lib/cachem,
|
||||
r-lib/rlang
|
||||
rstudio/bootstraplib
|
||||
URL: http://shiny.rstudio.com
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Collate:
|
||||
'globals.R'
|
||||
'app-state.R'
|
||||
'app_template.R'
|
||||
'bind-cache.R'
|
||||
'bind-event.R'
|
||||
'bookmark-state-local.R'
|
||||
'stack.R'
|
||||
'bookmark-state.R'
|
||||
@@ -138,8 +130,9 @@ Collate:
|
||||
'map.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'cache-disk.R'
|
||||
'cache-memory.R'
|
||||
'cache-utils.R'
|
||||
'deprecated.R'
|
||||
'diagnose.R'
|
||||
'fileupload.R'
|
||||
'font-awesome.R'
|
||||
@@ -204,7 +197,6 @@ Collate:
|
||||
'test-server.R'
|
||||
'test.R'
|
||||
'update-input.R'
|
||||
'utils-lang.R'
|
||||
'viewer.R'
|
||||
RoxygenNote: 7.1.1
|
||||
Encoding: UTF-8
|
||||
|
||||
46
NAMESPACE
46
NAMESPACE
@@ -25,22 +25,6 @@ S3method(as.shiny.appobj,list)
|
||||
S3method(as.shiny.appobj,shiny.appobj)
|
||||
S3method(as.tags,shiny.appobj)
|
||||
S3method(as.tags,shiny.render.function)
|
||||
S3method(bindCache,"function")
|
||||
S3method(bindCache,Observer)
|
||||
S3method(bindCache,default)
|
||||
S3method(bindCache,reactive.cache)
|
||||
S3method(bindCache,reactive.event)
|
||||
S3method(bindCache,reactiveExpr)
|
||||
S3method(bindCache,shiny.render.function)
|
||||
S3method(bindCache,shiny.render.function.cache)
|
||||
S3method(bindCache,shiny.render.function.event)
|
||||
S3method(bindCache,shiny.renderPlot)
|
||||
S3method(bindEvent,Observer)
|
||||
S3method(bindEvent,Observer.event)
|
||||
S3method(bindEvent,default)
|
||||
S3method(bindEvent,reactive.event)
|
||||
S3method(bindEvent,reactiveExpr)
|
||||
S3method(bindEvent,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(names,reactivevalues)
|
||||
@@ -66,8 +50,6 @@ export(animationOptions)
|
||||
export(appendTab)
|
||||
export(as.shiny.appobj)
|
||||
export(basicPage)
|
||||
export(bindCache)
|
||||
export(bindEvent)
|
||||
export(bookmarkButton)
|
||||
export(bootstrapLib)
|
||||
export(bootstrapPage)
|
||||
@@ -199,7 +181,6 @@ export(pre)
|
||||
export(prependTab)
|
||||
export(printError)
|
||||
export(printStackTrace)
|
||||
export(quoToFunction)
|
||||
export(radioButtons)
|
||||
export(reactive)
|
||||
export(reactiveConsole)
|
||||
@@ -336,9 +317,6 @@ import(httpuv)
|
||||
import(methods)
|
||||
import(mime)
|
||||
import(xtable)
|
||||
importFrom(digest,digest)
|
||||
importFrom(ellipsis,check_dots_empty)
|
||||
importFrom(ellipsis,check_dots_unnamed)
|
||||
importFrom(fastmap,fastmap)
|
||||
importFrom(fastmap,is.key_missing)
|
||||
importFrom(fastmap,key_missing)
|
||||
@@ -384,27 +362,3 @@ importFrom(htmltools,validateCssUnit)
|
||||
importFrom(htmltools,withTags)
|
||||
importFrom(promises,"%...!%")
|
||||
importFrom(promises,"%...>%")
|
||||
importFrom(promises,as.promise)
|
||||
importFrom(promises,is.promising)
|
||||
importFrom(promises,promise)
|
||||
importFrom(promises,promise_reject)
|
||||
importFrom(promises,promise_resolve)
|
||||
importFrom(rlang,"%||%")
|
||||
importFrom(rlang,as_function)
|
||||
importFrom(rlang,as_quosure)
|
||||
importFrom(rlang,enexpr)
|
||||
importFrom(rlang,enquo)
|
||||
importFrom(rlang,enquos)
|
||||
importFrom(rlang,enquos0)
|
||||
importFrom(rlang,eval_tidy)
|
||||
importFrom(rlang,expr)
|
||||
importFrom(rlang,get_env)
|
||||
importFrom(rlang,get_expr)
|
||||
importFrom(rlang,inject)
|
||||
importFrom(rlang,is_na)
|
||||
importFrom(rlang,is_quosure)
|
||||
importFrom(rlang,new_function)
|
||||
importFrom(rlang,new_quosure)
|
||||
importFrom(rlang,pairlist2)
|
||||
importFrom(rlang,quo)
|
||||
importFrom(rlang,zap_srcref)
|
||||
|
||||
6
NEWS.md
6
NEWS.md
@@ -54,12 +54,6 @@ shiny 1.5.0.9000
|
||||
|
||||
* `shinyOptions()` now has session-level scoping, in addition to global and application-level scoping. (#3080)
|
||||
|
||||
* `runApp()` now warns when running an application in an R package directory. (#3114)
|
||||
|
||||
* Shiny now uses `cache_mem` from the cachem package, instead of `memoryCache` and `diskCache`. (#3118)
|
||||
|
||||
* Closed #3140: Added support for `...` argument in `icon()`. (#3143)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed #2859: `renderPlot()` wasn't correctly setting `showtext::showtext_opts()`'s `dpi` setting with the correct resolution on high resolution displays; which means, if the font was rendered by showtext, font sizes would look smaller than they should on such displays. (#2941)
|
||||
|
||||
757
R/bind-cache.R
757
R/bind-cache.R
@@ -1,757 +0,0 @@
|
||||
utils::globalVariables(".GenericCallEnv", add = TRUE)
|
||||
|
||||
#' Add caching with reactivity to an object
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
|
||||
#' (like [renderText()], [renderTable()], ...).
|
||||
#'
|
||||
#' Ordinary [reactive()] expressions automatically cache their _most recent_
|
||||
#' value, which helps to avoid redundant computation in downstream reactives.
|
||||
#' `bindCache()` will cache all previous values (as long as they fit in the
|
||||
#' cache) and they can be shared across user sessions. This allows
|
||||
#' `bindCache()` to dramatically improve performance when used correctly.
|
||||
|
||||
#' @details
|
||||
#'
|
||||
#' `bindCache()` requires one or more expressions that are used to generate a
|
||||
#' **cache key**, which is used to determine if a computation has occurred
|
||||
#' before and hence can be retrieved from the cache. If you're familiar with the
|
||||
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
|
||||
#' can think of the cache key as the input(s) to a pure function. As such, one
|
||||
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
|
||||
#' sense, namely:
|
||||
#'
|
||||
#' 1. For a given key, the return value is always the same.
|
||||
#' 2. Evaluation has no side-effects.
|
||||
#'
|
||||
#' In the example here, the `bindCache()` key consists of `input$x` and
|
||||
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
|
||||
#' example, for any given key, there is only one possible returned value.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
|
||||
#' The largest performance improvements occur when the cache key is fast to
|
||||
#' compute and the reactive expression is slow to compute. To see if the value
|
||||
#' should be computed, a cached reactive evaluates the key, and then serializes
|
||||
#' and hashes the result. If the resulting hashed key is in the cache, then the
|
||||
#' cached reactive simply retrieves the previously calculated value and returns
|
||||
#' it; if not, then the value is computed and the result is stored in the cache
|
||||
#' before being returned.
|
||||
#'
|
||||
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
|
||||
#' best to avoid including large objects in a cache key since that can result in
|
||||
#' slow hashing. It's also best to avoid reference objects like environments and
|
||||
#' R6 objects, since the serialization of these objects may not capture relevant
|
||||
#' changes.
|
||||
#'
|
||||
#' If you want to use a large object as part of a cache key, it may make sense
|
||||
#' to do some sort of reduction on the data that still captures information
|
||||
#' about whether a value can be retrieved from the cache. For example, if you
|
||||
#' have a large data set with timestamps, it might make sense to extract the
|
||||
#' most recent timestamp and return that. Then, instead of hashing the entire
|
||||
#' data object, the cached reactive only needs to hash the timestamp.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ compute(bigdata()) } %>%
|
||||
#' bindCache({ extract_most_recent_time(bigdata()) })
|
||||
#' ```
|
||||
#'
|
||||
#' For computations that are vert slow, it often makes sense to pair
|
||||
#' [bindCache()] with [bindEvent()] so that no computation is performed until
|
||||
#' the user explicitly requests it (for more, see the Details section of
|
||||
#' [bindEvent()]).
|
||||
|
||||
#' @section Cache keys and reactivity:
|
||||
#'
|
||||
#' Because the **value** expression (from the original [reactive()]) is
|
||||
#' cached, it is not necessarily re-executed when someone retrieves a value,
|
||||
#' and therefore it can't be used to decide what objects to take reactive
|
||||
#' dependencies on. Instead, the **key** is used to figure out which objects
|
||||
#' to take reactive dependencies on. In short, the key expression is reactive,
|
||||
#' and value expression is no longer reactive.
|
||||
#'
|
||||
#' Here's an example of what not to do: if the key is `input$x` and the value
|
||||
#' expression is from `reactive({input$x + input$y})`, then the resulting
|
||||
#' cached reactive will only take a reactive dependency on `input$x` -- it
|
||||
#' won't recompute `{input$x + input$y}` when just `input$y` changes.
|
||||
#' Moreover, the cache won't use `input$y` as part of the key, and so it could
|
||||
#' return incorrect values in the future when it retrieves values from the
|
||||
#' cache. (See the examples below for an example of this.)
|
||||
#'
|
||||
#' A better cache key would be something like `input$x, input$y`. This does
|
||||
#' two things: it ensures that a reactive dependency is taken on both
|
||||
#' `input$x` and `input$y`, and it also makes sure that both values are
|
||||
#' represented in the cache key.
|
||||
#'
|
||||
#' In general, `key` should use the same reactive inputs as `value`, but the
|
||||
#' computation should be simpler. If there are other (non-reactive) values
|
||||
#' that are consumed, such as external data sources, they should be used in
|
||||
#' the `key` as well. Note that if the `key` is large, it can make sense to do
|
||||
#' some sort of reduction on it so that the serialization and hashing of the
|
||||
#' cache key is not too expensive.
|
||||
#'
|
||||
#' Remember that the key is _reactive_, so it is not re-executed every single
|
||||
#' time that someone accesses the cached reactive. It is only re-executed if
|
||||
#' it has been invalidated by one of the reactives it depends on. For
|
||||
#' example, suppose we have this cached reactive:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
#' In this case, the key expression is essentially `reactive(list(input$x,
|
||||
#' input$y))` (there's a bit more to it, but that's a good enough
|
||||
#' approximation). The first time `r()` is called, it executes the key, then
|
||||
#' fails to find it in the cache, so it executes the value expression, `{
|
||||
#' input$x + input$y }`. If `r()` is called again, then it does not need to
|
||||
#' re-execute the key expression, because it has not been invalidated via a
|
||||
#' change to `input$x` or `input$y`; it simply returns the previous value.
|
||||
#' However, if `input$x` or `input$y` changes, then the reactive expression will
|
||||
#' be invalidated, and the next time that someone calls `r()`, the key
|
||||
#' expression will need to be re-executed.
|
||||
#'
|
||||
#' Note that if the cached reactive is passed to [bindEvent()], then the key
|
||||
#' expression will no longer be reactive; instead, the event expression will be
|
||||
#' reactive.
|
||||
#'
|
||||
#'
|
||||
#' @section Cache scope:
|
||||
#'
|
||||
#' By default, when `bindCache()` is used, it is scoped to the running
|
||||
#' application. That means that it shares a cache with all user sessions
|
||||
#' connected to the application (within the R process). This is done with the
|
||||
#' `cache` parameter's default value, `"app"`.
|
||||
#'
|
||||
#' With an app-level cache scope, one user can benefit from the work done for
|
||||
#' another user's session. In most cases, this is the best way to get
|
||||
#' performance improvements from caching. However, in some cases, this could
|
||||
#' leak information between sessions. For example, if the cache key does not
|
||||
#' fully encompass the inputs used by the value, then data could leak between
|
||||
#' the sessions. Or if a user sees that a cached reactive returns its value
|
||||
#' very quickly, they may be able to infer that someone else has already used
|
||||
#' it with the same values.
|
||||
#'
|
||||
#' It is also possible to scope the cache to the session, with
|
||||
#' `cache="session"`. This removes the risk of information leaking between
|
||||
#' sessions, but then one session cannot benefit from computations performed in
|
||||
#' another session.
|
||||
#'
|
||||
#' It is possible to pass in caching objects directly to
|
||||
#' `bindCache()`. This can be useful if, for example, you want to use a
|
||||
#' particular type of cache with specific cached reactives, or if you want to
|
||||
#' use a [cachem::cache_disk()] that is shared across multiple processes and
|
||||
#' persists beyond the current R session.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 500 MB of space
|
||||
#' instead of the default 200 MB:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_mem(size = 500e6))
|
||||
#' ```
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can set
|
||||
#' `self$cache` at the top of your server function. By default, it will create
|
||||
#' a 200 MB memory cache for each session, but you can replace it with
|
||||
#' something different. To use the session-scoped cache, you must also call
|
||||
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
|
||||
#' the session:
|
||||
#'
|
||||
#' ```
|
||||
#' function(input, output, session) {
|
||||
#' session$cache <- cachem::cache_mem(size = 100e6)
|
||||
#' ...
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' If you want to use a cache that is shared across multiple R processes, you
|
||||
#' can use a [cachem::cache_disk()]. You can create a application-level shared
|
||||
#' cache by putting this at the top of your app.R, server.R, or global.R:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' ```
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' `myapp-cache` (replace `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:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk("./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 item, or selected items. To do that,
|
||||
#' create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
|
||||
#' as the `cache` argument of `bindCache()`.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Computing cache keys:
|
||||
#'
|
||||
#' The actual cache key that is used internally takes value from evaluating
|
||||
#' the key expression(s) (from the `...` arguments) and combines it with the
|
||||
#' (unevaluated) value expression.
|
||||
#'
|
||||
#' This means that if there are two cached reactives which have the same
|
||||
#' result from evaluating the key, but different value expressions, then they
|
||||
#' will not need to worry about collisions.
|
||||
#'
|
||||
#' However, if two cached reactives have identical key and value expressions
|
||||
#' expressions, they will share the cached values. This is useful when using
|
||||
#' `cache="app"`: there may be multiple user sessions which create separate
|
||||
#' cached reactive objects (because they are created from the same code in the
|
||||
#' server function, but the server function is executed once for each user
|
||||
#' session), and those cached reactive objects across sessions can share
|
||||
#' values in the cache.
|
||||
|
||||
|
||||
|
||||
#'
|
||||
#' @section Async with cached reactives:
|
||||
#'
|
||||
#' With a cached reactive expression, the key and/or value expression can be
|
||||
#' _asynchronous_. In other words, they can be promises --- not regular R
|
||||
#' promises, but rather objects provided by the
|
||||
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
|
||||
#' are similar to promises in JavaScript. (See [promises::promise()] for more
|
||||
#' information.) You can also use [future::future()] objects to run code in a
|
||||
#' separate process or even on a remote machine.
|
||||
#'
|
||||
#' If the value returns a promise, then anything that consumes the cached
|
||||
#' reactive must expect it to return a promise.
|
||||
#'
|
||||
#' Similarly, if the key is a promise (in other words, if it is asynchronous),
|
||||
#' then the entire cached reactive must be asynchronous, since the key must be
|
||||
#' computed asynchronously before it knows whether to compute the value or the
|
||||
#' value is retrieved from the cache. Anything that consumes the cached
|
||||
#' reactive must therefore expect it to return a promise.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Developing render functions for caching:
|
||||
#'
|
||||
#' If you've implemented your own `render*()` function, you may need to
|
||||
#' provide a `cacheHint` to [createRenderFunction()] (or
|
||||
#' [htmlwidgets::shinyRenderWidget()], if you've authored an htmlwidget) in
|
||||
#' order for `bindCache()` to correctly compute a cache key.
|
||||
#'
|
||||
#' The potential problem is a cache collision. Consider the following:
|
||||
#'
|
||||
#' ```
|
||||
#' output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
|
||||
#' output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
|
||||
#' ```
|
||||
#'
|
||||
#' Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
|
||||
#' but if it were the only thing used in the cache key, then the two outputs
|
||||
#' would have a cache collision, and they would have the same output. To avoid
|
||||
#' this, a _cache hint_ is automatically added when [renderText()] calls
|
||||
#' [createRenderFunction()]. The cache hint is used as part of the actual
|
||||
#' cache key, in addition to the one passed to `bindCache()` by the user. The
|
||||
#' cache hint can be viewed by calling the internal Shiny function
|
||||
#' `extractCacheHint()`:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- renderText({ input$x })
|
||||
#' shiny:::extractCacheHint(r)
|
||||
#' ```
|
||||
#'
|
||||
#' This returns a nested list containing an item, `$origUserFunc$body`, which
|
||||
#' in this case is the expression which was passed to `renderText()`:
|
||||
#' `{ input$x }`. This (quoted) expression is mixed into the actual cache
|
||||
#' key, and it is how `output$x1` does not have collisions with `output$x2`.
|
||||
#'
|
||||
#' For most developers of render functions, nothing extra needs to be done;
|
||||
#' the automatic inference of the cache hint is sufficient. Again, you can
|
||||
#' check it by calling `shiny:::extractCacheHint()`, and by testing the
|
||||
#' render function for cache collisions in a real application.
|
||||
#'
|
||||
#' In some cases, however, the automatic cache hint inference is not
|
||||
#' sufficient, and it is necessary to provide a cache hint. This is true
|
||||
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
|
||||
#' expression in another function, before passing it to [markRenderFunction()]
|
||||
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
|
||||
#' another function, markRenderFunction() is not able to automatically extract
|
||||
#' the user-provided code and use it in the cache key. Instead, `renderPrint`
|
||||
#' calls `markRenderFunction()`, it explicitly passes along a `cacheHint`,
|
||||
#' which includes a label and the original user expression.
|
||||
#'
|
||||
#' In general, if you need to provide a `cacheHint`, it is best practice to
|
||||
#' provide a `label` id, the user's `expr`, as well as any other arguments
|
||||
#' that may influence the final value.
|
||||
#'
|
||||
#' For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
|
||||
#' again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
|
||||
#' also test it in an application. If you do need to explicitly provide a
|
||||
#' cache hint, pass it to `shinyRenderWidget`. For example:
|
||||
#'
|
||||
#' ```
|
||||
#' renderMyWidget <- function(expr) {
|
||||
#' expr <- substitute(expr)
|
||||
#'
|
||||
#' htmlwidgets::shinyRenderWidget(expr,
|
||||
#' myWidgetOutput,
|
||||
#' quoted = TRUE,
|
||||
#' env = parent.frame(),
|
||||
#' cacheHint = list(label = "myWidget", userExpr = expr)
|
||||
#' )
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#'
|
||||
#' @section Uncacheable objects:
|
||||
#'
|
||||
#' Some render functions cannot be cached, typically because they have side
|
||||
#' effects or modify some external state, and they must re-execute each time
|
||||
#' in order to work properly.
|
||||
#'
|
||||
#' For developers of such code, they should call [createRenderFunction()] or
|
||||
#' [markRenderFunction()] with `cacheHint = FALSE`.
|
||||
#'
|
||||
#'
|
||||
#' @section Caching with `renderPlot()`:
|
||||
#'
|
||||
#' When `bindCache()` is used with `renderPlot()`, the `height` and `width`
|
||||
#' passed to the original `renderPlot()` are ignored. They are superseded by
|
||||
#' `sizePolicy` argument passed to `bindCache. The default is:
|
||||
#'
|
||||
#' ```
|
||||
#' sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
#' ```
|
||||
#'
|
||||
#' `sizePolicy` must be a function that takes a two-element numeric vector as
|
||||
#' input, representing the width and height of the `<img>` element in the
|
||||
#' browser window, and it must return a two-element numeric vector, representing
|
||||
#' the pixel dimensions of the plot to generate. 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 [sizeGrowthRatio()] for more information on the default sizing policy.
|
||||
#'
|
||||
#' @param x The object to add caching to.
|
||||
#' @param ... One or more expressions to use in the caching key.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
|
||||
#' (the default), `"session"`, or a cache object like a
|
||||
#' [cachem::cache_disk()]. See the Cache Scoping section for more information.
|
||||
#'
|
||||
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' rc <- bindCache(
|
||||
#' x = reactive({
|
||||
#' Sys.sleep(2) # Pretend this is expensive
|
||||
#' input$x * 100
|
||||
#' }),
|
||||
#' input$x
|
||||
#' )
|
||||
#'
|
||||
#' # Can make it prettier with the %>% operator
|
||||
#' library(magrittr)
|
||||
#'
|
||||
#' rc <- reactive({
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * 100
|
||||
#' }) %>%
|
||||
#' bindCache(input$x)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run app examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Basic example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' # The value expression is an _expensive_ computation
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Caching renderText
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$txt <- renderText({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Demo of using events and caching with an actionButton
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' actionButton("go", "Go"),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' # The cached, eventified reactive takes a reactive dependency on
|
||||
#' # input$go, but doesn't use it for the cache key. It uses input$x and
|
||||
#' # input$y for the cache key, but doesn't take a reactive depdency on
|
||||
#' # them, because the reactive dependency is superseded by addEvent().
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
bindCache <- function(x, ..., cache = "app") {
|
||||
force(cache)
|
||||
|
||||
UseMethod("bindCache")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
label <- exprToLabel(substitute(key), "cachedReactive")
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
# Convert the ... to a function that returns their evaluated values.
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
# Hash cache hint now -- this will be added to the key later on, to reduce the
|
||||
# chance of key collisions with other cachedReactives.
|
||||
cacheHint <- digest(extractCacheHint(x), algo = "spookyhash")
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
# Hacky workaround for issue with `%>%` preventing GC:
|
||||
# https://github.com/tidyverse/magrittr/issues/229
|
||||
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv)) {
|
||||
rm(list = ".", envir = .GenericCallEnv)
|
||||
}
|
||||
|
||||
|
||||
res <- reactive(label = label, domain = domain, {
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
|
||||
)
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.cache", class(res))
|
||||
res
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
cacheHint <- digest(extractCacheHint(x), algo = "spookyhash")
|
||||
|
||||
cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
|
||||
cacheReadHook <- attr(x, "cacheReadHook", exact = TRUE) %||% identity
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
renderFunc <- function(...) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.renderPlot <- function(x, ...,
|
||||
cache = "app",
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
|
||||
{
|
||||
check_dots_unnamed()
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
# Given the actual width/height of the image element in the browser, the
|
||||
# resize observer computes the width/height using sizePolicy() and pushes
|
||||
# those values into `fitWidth` and `fitHeight`. It's done this way so that the
|
||||
# `fitWidth` and `fitHeight` only change (and cause invalidations of the key
|
||||
# expression) when the rendered image size changes, and not every time the
|
||||
# browser's <img> tag changes size.
|
||||
#
|
||||
# If the key expression were invalidated every time the image element changed
|
||||
# size, even if the resulting key was the same (because `sizePolicy()` gave
|
||||
# the same output for a slightly different img element size), it would result
|
||||
# in getting the (same) image from the cache and sending it to the client
|
||||
# again. This resize observer prevents that.
|
||||
fitDims <- reactiveVal(NULL)
|
||||
resizeObserverCreated <- FALSE
|
||||
outputName <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (resizeObserverCreated)
|
||||
return()
|
||||
|
||||
doResizeCheck <- function() {
|
||||
if (is.null(outputName)) {
|
||||
outputName <<- getCurrentOutputInfo()$name
|
||||
}
|
||||
session <- getDefaultReactiveDomain()
|
||||
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]] %||% 0
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims(list(width = rect[1], height = rect[2]))
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
observe({
|
||||
doResizeCheck()
|
||||
})
|
||||
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
|
||||
# Currently, if you reassign output$foo, the observer persists until the
|
||||
# session ends. This is generally bad programming practice and should be
|
||||
# rare, but still, we should try to clean up properly.
|
||||
|
||||
resizeObserverCreated <<- TRUE
|
||||
}
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
# Pass in fitDims so that so that the generated plot will be the
|
||||
# dimensions specified by the sizePolicy; otherwise the plot would be the
|
||||
# exact dimensions of the img element, which isn't what we want for cached
|
||||
# plots.
|
||||
valueFunc(..., get_dims = fitDims),
|
||||
function(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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- class(valueFunc)
|
||||
|
||||
bindCache.shiny.render.function(
|
||||
renderFunc,
|
||||
...,
|
||||
{
|
||||
ensureResizeObserver()
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (is.null(session) || is.null(fitDims())) {
|
||||
req(FALSE)
|
||||
}
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
|
||||
list(fitDims(), pixelratio)
|
||||
},
|
||||
cache = cache
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.cache <- function(x, ...) {
|
||||
stop("bindCache() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.cache <- bindCache.reactive.cache
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.event <- function(x, ...) {
|
||||
stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.event <- bindCache.reactive.event
|
||||
|
||||
#' @export
|
||||
bindCache.Observer <- function(x, ...) {
|
||||
stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.function <- function(x, ...) {
|
||||
stop(
|
||||
"Don't know how to add caching to a plain function. ",
|
||||
"If this is a render* function for Shiny, it may need to be updated. ",
|
||||
"Please see ?shiny::bindCache for more information."
|
||||
)
|
||||
}
|
||||
|
||||
# Returns a function which should be passed as a step in to hybrid_chain(). The
|
||||
# returned function takes a cache key as input and manages storing and retrieving
|
||||
# values from the cache, as well as executing the valueFunc if needed.
|
||||
generateCacheFun <- function(
|
||||
valueFunc,
|
||||
cache,
|
||||
cacheHint,
|
||||
cacheReadHook,
|
||||
cacheWriteHook,
|
||||
...
|
||||
) {
|
||||
function(cacheKeyResult) {
|
||||
key_str <- digest(list(cacheKeyResult, cacheHint), algo = "spookyhash")
|
||||
res <- cache$get(key_str)
|
||||
|
||||
# Case 1: cache hit
|
||||
if (!is.key_missing(res)) {
|
||||
return(hybrid_chain(
|
||||
{
|
||||
# The first step is just to convert `res` to a promise or not, so
|
||||
# that hybrid_chain() knows to propagate the promise-ness.
|
||||
if (res$is_promise) promise_resolve(res)
|
||||
else res
|
||||
},
|
||||
function(res) {
|
||||
if (res$error) {
|
||||
stop(res$value)
|
||||
}
|
||||
|
||||
cacheReadHook(valueWithVisible(res))
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
# Case 2: cache miss
|
||||
#
|
||||
# valueFunc() might return a promise, or an actual value. Normally we'd
|
||||
# use a hybrid_chain() for this, but in this case, we need to have
|
||||
# different behavior if it's a promise or not a promise -- the
|
||||
# information about whether or not it's a promise needs to be stored in
|
||||
# the cache. We need to handle both cases and record in the cache
|
||||
# whether it's a promise or not, so that any consumer of the
|
||||
# cachedReactive() will be given the correct kind of object (a promise
|
||||
# vs. an actual value) in the case of a future cache hit.
|
||||
p <- withCallingHandlers(
|
||||
withVisible(isolate(valueFunc(...))),
|
||||
error = function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
if (is.promising(p$value)) {
|
||||
p$value <- as.promise(p$value)
|
||||
p$value <- p$value$
|
||||
then(function(value) {
|
||||
res <- withVisible(value)
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = cacheWriteHook(res$value),
|
||||
visible = res$visible,
|
||||
error = FALSE
|
||||
))
|
||||
valueWithVisible(res)
|
||||
})$
|
||||
catch(function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
stop(e)
|
||||
})
|
||||
valueWithVisible(p)
|
||||
} else {
|
||||
# result is an ordinary value, not a promise.
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = cacheWriteHook(p$value),
|
||||
visible = p$visible,
|
||||
error = FALSE
|
||||
))
|
||||
return(valueWithVisible(p))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractCacheHint <- function(func) {
|
||||
cacheHint <- attr(func, "cacheHint", exact = TRUE)
|
||||
|
||||
if (is_false(cacheHint)) {
|
||||
stop(
|
||||
"Cannot call `bindCache()` on this object because it is marked as not cacheable.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (is.null(cacheHint)) {
|
||||
warning("No cacheHint found for this object. ",
|
||||
"Caching may not work properly.")
|
||||
}
|
||||
|
||||
cacheHint
|
||||
}
|
||||
308
R/bind-event.R
308
R/bind-event.R
@@ -1,308 +0,0 @@
|
||||
#' Make an object respond only to specified reactive events
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' Modify an object to respond to "event-like" reactive inputs, values, and
|
||||
#' expressions. `bindEvent()` can be used with reactive expressions, render
|
||||
#' functions, and observers. The resulting object takes a reactive dependency on
|
||||
#' the `...` arguments, and not on the original object's code. This can, for
|
||||
#' example, be used to make an observer execute only when a button is pressed.
|
||||
#'
|
||||
#' @section Details:
|
||||
#'
|
||||
#' Shiny's reactive programming framework is primarily designed for calculated
|
||||
#' values (reactive expressions) and side-effect-causing actions (observers)
|
||||
#' that respond to *any* of their inputs changing. That's often what is
|
||||
#' desired in Shiny apps, but not always: sometimes you want to wait for a
|
||||
#' specific action to be taken from the user, like clicking an
|
||||
#' [actionButton()], before calculating an expression or taking an action. A
|
||||
#' reactive value or expression that is used to trigger other calculations in
|
||||
#' this way is called an *event*.
|
||||
#'
|
||||
#' These situations demand a more imperative, "event handling" style of
|
||||
#' programming that is possible--but not particularly intuitive--using the
|
||||
#' reactive programming primitives [observe()] and [isolate()]. `bindEvent()`
|
||||
#' provides a straightforward API for event handling that wraps `observe` and
|
||||
#' `isolate`.
|
||||
#'
|
||||
#' The `...` arguments are captured as expressions and combined into an
|
||||
#' **event expression**. When this event expression is invalidated (when its
|
||||
#' upstream reactive inputs change), that is an **event**, and it will cause
|
||||
#' the original object's code to execute.
|
||||
#'
|
||||
#' Use `bindEvent()` with `observe()` whenever you want to *perform an action*
|
||||
#' in response to an event. (Note that "recalculate a value" does not
|
||||
#' generally count as performing an action -- use [reactive()] for that.) The
|
||||
#' first argument is observer whose code should be executed whenever the event
|
||||
#' occurs.
|
||||
#'
|
||||
#' Use `bindEvent()` with `reactive()` to create a *calculated value* that only
|
||||
#' updates in response to an event. This is just like a normal [reactive
|
||||
#' expression][reactive] except it ignores all the usual invalidations that
|
||||
#' come from its reactive dependencies; it only invalidates in response to the
|
||||
#' given event.
|
||||
#'
|
||||
#' `bindEvent()` is often used with [bindCache()].
|
||||
#'
|
||||
#' @section ignoreNULL and ignoreInit:
|
||||
#'
|
||||
#' `bindEvent()` takes an `ignoreNULL` parameter that affects behavior when
|
||||
#' the event expression evaluates to `NULL` (or in the special case of an
|
||||
#' [actionButton()], `0`). In these cases, if `ignoreNULL` is `TRUE`, then it
|
||||
#' will raise a silent [validation][validate] error. This is useful behavior
|
||||
#' if you don't want to do the action or calculation when your app first
|
||||
#' starts, but wait for the user to initiate the action first (like a "Submit"
|
||||
#' button); whereas `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).
|
||||
#'
|
||||
#' `bindEvent()` also takes an `ignoreInit` argument. By default, reactive
|
||||
#' expressions and observers will run on the first reactive flush after they
|
||||
#' are created (except if, at that moment, the event expression evaluates to
|
||||
#' `NULL` and `ignoreNULL` is `TRUE`). But when responding to a click of an
|
||||
#' action button, it may often be useful to set `ignoreInit` to `TRUE`. For
|
||||
#' example, if you're setting up an observer to respond to a dynamically
|
||||
#' created button, then `ignoreInit = TRUE` will guarantee that the action
|
||||
#' 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 a reactive that responds to a dynamically created button used to
|
||||
#' refresh some data (which is then returned by that `reactive`), then you
|
||||
#' should use `reactive(...) %>% bindEvent(..., 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).
|
||||
#'
|
||||
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar purposes
|
||||
#' they are independent from one another. Here's the result of combining
|
||||
#' these:
|
||||
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
|
||||
#' This is the default. This combination means that reactive/observer code
|
||||
#' will run every time that event expression is not
|
||||
#' `NULL`. If, at the time of creation, the event expression happens
|
||||
#' to *not* be `NULL`, then the code runs.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = FALSE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' run every time no matter what.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* run at the time of creation (because `ignoreInit = TRUE`),
|
||||
#' but it will run every other time.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* at the time of creation (because `ignoreInit = TRUE`).
|
||||
#' After that, the reactive/observer code will run every time that
|
||||
#' the event expression is not `NULL`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
|
||||
#' @section Types of objects:
|
||||
#'
|
||||
#' `bindEvent()` can be used with reactive expressions, observers, and shiny
|
||||
#' render functions.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `reactive()`, it creates a new reactive
|
||||
#' expression object.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `observe()`, it alters the observer in
|
||||
#' place. It can only be used with observers which have not yet executed.
|
||||
#'
|
||||
#' @section Combining events and caching:
|
||||
#'
|
||||
#' In many cases, it makes sense to use `bindEvent()` along with
|
||||
#' `bindCache()`, because they each can reduce the amount of work done on the
|
||||
#' server. For example, you could have [sliderInput]s `x` and `y` and a
|
||||
#' `reactive()` that performs a time-consuming operation with those values.
|
||||
#' Using `bindCache()` can speed things up, especially if there are multiple
|
||||
#' users. But it might make sense to also not do the computation until the
|
||||
#' user sets both `x` and `y`, and then clicks on an [actionButton] named
|
||||
#' `go`.
|
||||
#'
|
||||
#' To use both caching and events, the object should first be passed to
|
||||
#' `bindCache()`, then `bindEvent()`. For example:
|
||||
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({
|
||||
#' Sys.sleep(2) # Pretend this is an expensive computation
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' ```
|
||||
|
||||
#'
|
||||
#' Anything that consumes `r()` will take a reactive dependency on the event
|
||||
#' expression given to `bindEvent()`, and not the cache key expression given to
|
||||
#' `bindCache()`. In this case, it is just `input$go`.
|
||||
#'
|
||||
#' @param x An object to wrap so that is triggered only when a the specified
|
||||
#' event occurs.
|
||||
#' @param ignoreNULL Whether the action should be triggered (or value
|
||||
#' calculated) when the input is `NULL`. See Details.
|
||||
#' @param ignoreInit If `TRUE`, then, when the eventified object is first
|
||||
#' created/initialized, don't trigger the action or (compute the value). The
|
||||
#' default is `FALSE`. See Details.
|
||||
#' @param once Used only for observers. Whether this `observer` should be
|
||||
#' immediately destroyed after the first time that the code in the observer is
|
||||
#' run. This pattern is useful when you want to subscribe to a event that
|
||||
#' should only happen once.
|
||||
#' @param label A label for the observer or reactive, useful for debugging.
|
||||
#' @param ... One or more expressions that represents the event; this can be a
|
||||
#' simple reactive value like `input$click`, a call to a reactive expression
|
||||
#' like `dataset()`, or even a complex expression inside curly braces. If
|
||||
#' there are multiple expressions in the `...`, then it will take a dependency
|
||||
#' on all of them.
|
||||
#' @export
|
||||
bindEvent <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
check_dots_unnamed()
|
||||
force(ignoreNULL)
|
||||
force(ignoreInit)
|
||||
force(once)
|
||||
|
||||
UseMethod("bindEvent")
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
label = NULL)
|
||||
{
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
label <- label %||%
|
||||
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.event", class(res))
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
eventFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc(...))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
if (x$.execCount > 0) {
|
||||
stop("Cannot call bindEvent() on an Observer that has already been executed.")
|
||||
}
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
valueFunc <- x$.func
|
||||
|
||||
# Note that because the observer will already have been logged by this point,
|
||||
# this updated label won't show up in the reactlog.
|
||||
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
x$.func <- wrapFunctionLabel(
|
||||
name = x$.label,
|
||||
..stacktraceon = FALSE,
|
||||
func = function() {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(x$destroy())
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
class(x) <- c("Observer.event", class(x))
|
||||
invisible(x)
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactive.event <- function(x, ...) {
|
||||
stop("bindEvent() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer.event <- bindEvent.reactive.event
|
||||
@@ -79,7 +79,7 @@ saveShinySaveState <- function(state) {
|
||||
|
||||
# Look for a save.interface function. This will be defined by the hosting
|
||||
# environment if it supports bookmarking.
|
||||
saveInterface <- getShinyOption("save.interface", default = NULL)
|
||||
saveInterface <- getShinyOption("save.interface")
|
||||
|
||||
if (is.null(saveInterface)) {
|
||||
if (inShinyServer()) {
|
||||
@@ -296,7 +296,7 @@ RestoreContext <- R6Class("RestoreContext",
|
||||
|
||||
# Look for a load.interface function. This will be defined by the hosting
|
||||
# environment if it supports bookmarking.
|
||||
loadInterface <- getShinyOption("load.interface", default = NULL)
|
||||
loadInterface <- getShinyOption("load.interface")
|
||||
|
||||
if (is.null(loadInterface)) {
|
||||
if (inShinyServer()) {
|
||||
|
||||
@@ -440,7 +440,7 @@ verticalLayout <- function(..., fluid = TRUE) {
|
||||
flowLayout <- function(..., cellArgs = list()) {
|
||||
|
||||
children <- list(...)
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
childIdx <- !nzchar(names(children) %OR% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
|
||||
@@ -523,7 +523,7 @@ inputPanel <- function(...) {
|
||||
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
|
||||
children <- list(...)
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
childIdx <- !nzchar(names(children) %OR% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
count <- length(children)
|
||||
|
||||
@@ -18,7 +18,7 @@ NULL
|
||||
#' Bootstrap 3.
|
||||
#' @param theme One of the following:
|
||||
#' * `NULL` (the default), which implies a "stock" build of Bootstrap 3.
|
||||
#' * A [bslib::bs_theme()] object. This can be used to replace a stock
|
||||
#' * A [bootstraplib::bs_theme()] object. This can be used to replace a stock
|
||||
#' build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
|
||||
#' * A character string pointing to an alternative Bootstrap stylesheet
|
||||
#' (normally a css file within the www directory, e.g. `www/bootstrap.css`).
|
||||
@@ -78,7 +78,7 @@ getLang <- function(ui) {
|
||||
#' @export
|
||||
bootstrapLib <- function(theme = NULL) {
|
||||
tagFunction(function() {
|
||||
# If we're not compiling Bootstrap Sass (from bslib), return the
|
||||
# If we're not compiling Bootstrap Sass (from bootstraplib), return the
|
||||
# static Bootstrap build.
|
||||
if (!is_bs_theme(theme)) {
|
||||
# We'll enter here if `theme` is the path to a .css file, like that
|
||||
@@ -100,7 +100,8 @@ bootstrapLib <- function(theme = NULL) {
|
||||
# option is automatically reset when the app (or session) exits
|
||||
if (isRunning()) {
|
||||
setCurrentTheme(theme)
|
||||
registerThemeDependency(bs_theme_deps)
|
||||
print("is running! and registering bs_theme_dependencies_css")
|
||||
registerThemeDependency(bs_theme_dependencies_css)
|
||||
|
||||
} else {
|
||||
# Technically, this a potential issue (someone trying to execute/render
|
||||
@@ -119,19 +120,22 @@ bootstrapLib <- function(theme = NULL) {
|
||||
#)
|
||||
}
|
||||
|
||||
bslib::bs_theme_dependencies(theme)
|
||||
bootstraplib::bs_theme_dependencies(theme)
|
||||
})
|
||||
}
|
||||
|
||||
# This is defined outside of bootstrapLib() because registerThemeDependency()
|
||||
# wants a non-anonymous function with a single argument
|
||||
bs_theme_deps <- function(theme) {
|
||||
bslib::bs_theme_dependencies(theme)
|
||||
# wants non-anonymous functions.
|
||||
bs_theme_dependencies_css <- function(theme) {
|
||||
deps <- bootstraplib::bs_theme_dependencies(theme)
|
||||
# Extract out the CSS files only (no need to re-send JS files, even though
|
||||
# they wouldn't be re-rendered on the client anyway.)
|
||||
Filter(deps, f = function(dep) !is.null(dep$stylesheet))
|
||||
}
|
||||
|
||||
is_bs_theme <- function(x) {
|
||||
is_available("bslib", "0.2.0.9000") &&
|
||||
bslib::is_bs_theme(x)
|
||||
is_available("bootstraplib", "0.2.0.9000") &&
|
||||
bootstraplib::is_bs_theme(x)
|
||||
}
|
||||
|
||||
#' Obtain Shiny's Bootstrap Sass theme
|
||||
@@ -140,14 +144,14 @@ is_bs_theme <- function(x) {
|
||||
#' styling based on the [bootstrapLib()]'s `theme` value.
|
||||
#'
|
||||
#' @return If called at render-time (i.e., inside a [htmltools::tagFunction()]),
|
||||
#' and [bootstrapLib()]'s `theme` has been set to a [bslib::bs_theme()]
|
||||
#' and [bootstrapLib()]'s `theme` has been set to a [bootstraplib::bs_theme()]
|
||||
#' object, then this returns the `theme`. Otherwise, this returns `NULL`.
|
||||
#' @seealso [getCurrentOutputInfo()], [bootstrapLib()], [htmltools::tagFunction()]
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
getCurrentTheme <- function() {
|
||||
getShinyOption("bootstrapTheme", default = NULL)
|
||||
getShinyOption("bootstrapTheme")
|
||||
}
|
||||
|
||||
setCurrentTheme <- function(theme) {
|
||||
@@ -185,13 +189,14 @@ registerThemeDependency <- function(func) {
|
||||
|
||||
# Note that this will automatically scope to the app or session level,
|
||||
# depending on if this is called from within a session or not.
|
||||
funcs <- getShinyOption("themeDependencyFuncs", default = list())
|
||||
|
||||
funcs <- getShinyOption("themeDependencyFuncs", list())
|
||||
str(funcs)
|
||||
# Don't add func if it's already present.
|
||||
have_func <- any(vapply(funcs, identical, logical(1), func))
|
||||
if (!have_func) {
|
||||
funcs[[length(funcs) + 1]] <- func
|
||||
}
|
||||
str(funcs)
|
||||
|
||||
shinyOptions("themeDependencyFuncs" = funcs)
|
||||
}
|
||||
@@ -209,7 +214,7 @@ bootstrapDependency <- function(theme) {
|
||||
"accessibility/js/bootstrap-accessibility.min.js"
|
||||
),
|
||||
stylesheet = c(
|
||||
theme %||% "css/bootstrap.min.css",
|
||||
theme %OR% "css/bootstrap.min.css",
|
||||
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
|
||||
"accessibility/css/bootstrap-accessibility.css"
|
||||
),
|
||||
@@ -868,7 +873,7 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
|
||||
foundSelected <<- TRUE
|
||||
div <- markTabAsSelected(div)
|
||||
} else {
|
||||
tabValue <- div$attribs$`data-value` %||% div$attribs$title
|
||||
tabValue <- div$attribs$`data-value` %OR% div$attribs$title
|
||||
if (identical(selected, tabValue)) {
|
||||
foundSelected <<- TRUE
|
||||
div <- markTabAsSelected(div)
|
||||
@@ -1531,7 +1536,6 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
|
||||
#' [usage examples](http://fontawesome.io/examples/) for details on
|
||||
#' supported styles).
|
||||
#' @param lib Icon library to use ("font-awesome" or "glyphicon")
|
||||
#' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]
|
||||
#'
|
||||
#' @return An icon element
|
||||
#'
|
||||
@@ -1550,7 +1554,7 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
|
||||
#' tabPanel("Table", icon = icon("table"))
|
||||
#' )
|
||||
#' @export
|
||||
icon <- function(name, class = NULL, lib = "font-awesome", ...) {
|
||||
icon <- function(name, class = NULL, lib = "font-awesome") {
|
||||
prefixes <- list(
|
||||
"font-awesome" = "fa",
|
||||
"glyphicon" = "glyphicon"
|
||||
@@ -1576,7 +1580,7 @@ icon <- function(name, class = NULL, lib = "font-awesome", ...) {
|
||||
if (!is.null(class))
|
||||
iconClass <- paste(iconClass, class)
|
||||
|
||||
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"), ...)
|
||||
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"))
|
||||
|
||||
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
|
||||
if (lib == "font-awesome") {
|
||||
|
||||
567
R/cache-disk.R
Normal file
567
R/cache-disk.R
Normal file
@@ -0,0 +1,567 @@
|
||||
#' 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 `get()`
|
||||
#' and `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing Keys:
|
||||
#'
|
||||
#' The `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *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 [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `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 `get()` encounters missing
|
||||
#' key. If the function returns a value, then `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 `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `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 `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' The disk cache will throttle the pruning so that it does not happen on
|
||||
#' every call to `set()`, because the filesystem operations for checking
|
||||
#' the status of files can be slow. Instead, it will prune once in every 20
|
||||
#' calls to `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
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `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
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"lru"`}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's mtime property. When "lru" is used, each
|
||||
#' `get()` is called, it will update the file's mtime.
|
||||
#' }
|
||||
#' \item{`"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 `exists(key)` to check
|
||||
#' if an object is in the cache, and then call `get(key)`, the object may
|
||||
#' be removed from the cache in between those two calls, and `get(key)`
|
||||
#' will throw an error. Instead of calling the two functions, it is better to
|
||||
#' simply call `get(key)`, and check that the returned object is not a
|
||||
#' `key_missing()` object, using `is.key_missing()`. 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 `file.remove()` failing to remove a file that has
|
||||
#' already been deleted.
|
||||
#'
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `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{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `evict`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @param dir Directory to store files for the cache. If `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 `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
|
||||
#' `evict`. Use `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 `evict`. Use `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, `"lru"` and `"fifo"` are
|
||||
#' supported.
|
||||
#' @param destroy_on_finalize If `TRUE`, then when the DiskCache object is
|
||||
#' garbage collected, the cache directory and all objects inside of it will be
|
||||
#' deleted from disk. If `FALSE` (the default), it will do nothing when
|
||||
#' finalized.
|
||||
#' @param missing A value to return or a function to execute when
|
||||
#' `get(key)` is called but the key is not present in the cache. The
|
||||
#' default is a [key_missing()] object. If it is a function to
|
||||
#' execute, the function must take one argument (the key), and you must also
|
||||
#' use `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
|
||||
#' `get()`. See section Missing keys for more information.
|
||||
#' @param exec_missing If `FALSE` (the default), then treat `missing`
|
||||
#' as a value to return when `get()` results in a cache miss. If
|
||||
#' `TRUE`, treat `missing` as a function to execute when
|
||||
#' `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 `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('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 = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
# This maps rm_success back into the TRUEs in the rm_idx vector.
|
||||
# If (for example) rm_idx is c(F,T,F,T,T) and rm_success is c(T,F,T),
|
||||
# then this line modifies rm_idx to be c(F,T,F,F,T).
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
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])
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 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])
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
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)
|
||||
cat(text, sep = "\n", file = private$logfile, append = TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
365
R/cache-memory.R
Normal file
365
R/cache-memory.R
Normal file
@@ -0,0 +1,365 @@
|
||||
#' 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 `get()` and
|
||||
#' `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#' In a `MemoryCache`, R objects are stored directly in the cache; they are
|
||||
#' not *not* serialized before being stored in the cache. This contrasts
|
||||
#' with other cache types, like [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 `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *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 [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `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 `get()` encounters missing
|
||||
#' key. If the function returns a value, then `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 `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `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 `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `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
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"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{`"fifo"`}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `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{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `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 <- fastmap()
|
||||
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$get(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$set(key, list(
|
||||
key = key,
|
||||
value = value,
|
||||
size = size,
|
||||
mtime = time,
|
||||
atime = time
|
||||
))
|
||||
self$prune()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
validate_key(key)
|
||||
private$cache$has(key)
|
||||
},
|
||||
|
||||
keys = function() {
|
||||
private$cache$keys()
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
validate_key(key)
|
||||
private$cache$remove(key)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
private$cache$reset()
|
||||
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 = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
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 = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
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 = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
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$get(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))
|
||||
private$cache$remove(key)
|
||||
}
|
||||
},
|
||||
|
||||
object_info = function() {
|
||||
keys <- private$cache$keys()
|
||||
data.frame(
|
||||
key = keys,
|
||||
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
|
||||
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
|
||||
atime = vapply(keys, function(key) private$cache$get(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)
|
||||
cat(text, sep = "\n", file = private$logfile, append = TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,25 +1,9 @@
|
||||
# For our purposes, cache objects must support these methods.
|
||||
is_cache_object <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Given a cache object, or string "app" or "session", return appropriate cache
|
||||
# object.
|
||||
resolve_cache_object <- function(cache, session) {
|
||||
if (identical(cache, "app")) {
|
||||
cache <- getShinyOption("cache", default = NULL)
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <- session$cache
|
||||
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 (is_cache_object(cache)) {
|
||||
return(cache)
|
||||
if (grepl("[^a-z0-9]", key)) {
|
||||
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
|
||||
}
|
||||
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
|
||||
102
R/deprecated.R
102
R/deprecated.R
@@ -1,102 +0,0 @@
|
||||
#' Print message for deprecated functions in Shiny
|
||||
#'
|
||||
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
|
||||
#'
|
||||
#' @param new Name of replacement function.
|
||||
#' @param msg Message to print. If used, this will override the default message.
|
||||
#' @param old Name of deprecated function.
|
||||
#' @param version The last version of Shiny before the item was deprecated.
|
||||
#' @keywords internal
|
||||
shinyDeprecated <- function(new=NULL, msg=NULL,
|
||||
old=as.character(sys.call(sys.parent()))[1L],
|
||||
version = NULL) {
|
||||
|
||||
if (getOption("shiny.deprecation.messages") %||% TRUE == FALSE)
|
||||
return(invisible())
|
||||
|
||||
if (is.null(msg)) {
|
||||
msg <- paste(old, "is deprecated.")
|
||||
if (!is.null(new)) {
|
||||
msg <- paste(msg, "Please use", new, "instead.",
|
||||
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
|
||||
}
|
||||
}
|
||||
|
||||
if (!is.null(version)) {
|
||||
msg <- paste0(msg, " (Last used in version ", version, ")")
|
||||
}
|
||||
|
||||
# Similar to .Deprecated(), but print a message instead of warning
|
||||
message(msg)
|
||||
}
|
||||
|
||||
|
||||
deprecatedEnvQuotedMessage <- function(env_arg = "env", quoted_arg = "quoted") {
|
||||
# Enable this message in a future version of Shiny, perhaps in a dev_edition()
|
||||
# mode.
|
||||
# shinyDeprecated(msg = paste(
|
||||
# sprintf("The `%s` and `%s` arguments are deprecated.", env_arg, quoted_arg),
|
||||
# "Please use quosures from rlang instead.",
|
||||
# "See https://github.com/rstudio/shiny/issues/3108 for more information."
|
||||
# ))
|
||||
}
|
||||
|
||||
|
||||
#' Create disk cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_disk
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 500 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
shinyDeprecated("cachem::cache_disk", version = "1.5.1")
|
||||
|
||||
cachem::cache_disk(
|
||||
dir = dir,
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
destroy_on_finalize = destroy_on_finalize,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Create memory cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_mem
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 200 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
shinyDeprecated("cachem::cache_mem", version = "1.5.1")
|
||||
|
||||
cachem::cache_mem(
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ check_suggested <- function(package, version = NULL) {
|
||||
|
||||
msg <- paste0(
|
||||
sQuote(package),
|
||||
if (is.na(version %||% NA)) "" else paste0("(>= ", version, ")"),
|
||||
if (is.na(version %OR% NA)) "" else paste0("(>= ", version, ")"),
|
||||
" must be installed for this functionality."
|
||||
)
|
||||
|
||||
|
||||
@@ -92,21 +92,11 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
use_x <- grepl("x", brush$direction)
|
||||
use_y <- grepl("y", brush$direction)
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from brush object
|
||||
xvar <- xvar %||% brush$mapping$x
|
||||
yvar <- yvar %||% brush$mapping$y
|
||||
panelvar1 <- panelvar1 %||% brush$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% brush$mapping$panelvar2
|
||||
xvar <- xvar %OR% brush$mapping$x
|
||||
yvar <- yvar %OR% brush$mapping$y
|
||||
panelvar1 <- panelvar1 %OR% brush$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %OR% brush$mapping$panelvar2
|
||||
|
||||
# Filter out x and y values
|
||||
keep_rows <- rep(TRUE, nrow(df))
|
||||
@@ -240,21 +230,11 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
stop("nearPoints requires a click/hover/double-click object with x and y values.")
|
||||
}
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from coordinfo object
|
||||
xvar <- xvar %||% coordinfo$mapping$x
|
||||
yvar <- yvar %||% coordinfo$mapping$y
|
||||
panelvar1 <- panelvar1 %||% coordinfo$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% coordinfo$mapping$panelvar2
|
||||
xvar <- xvar %OR% coordinfo$mapping$x
|
||||
yvar <- yvar %OR% coordinfo$mapping$y
|
||||
panelvar1 <- panelvar1 %OR% coordinfo$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %OR% coordinfo$mapping$panelvar2
|
||||
|
||||
if (is.null(xvar))
|
||||
stop("nearPoints: not able to automatically infer `xvar` from coordinfo")
|
||||
|
||||
@@ -4,12 +4,12 @@ startPNG <- function(filename, width, height, res, ...) {
|
||||
# to use ragg (say, instead of showtext, for custom font rendering).
|
||||
# In the next shiny release, this option will likely be superseded in
|
||||
# favor of a fully customizable graphics device option
|
||||
if ((getOption('shiny.useragg') %||% FALSE) && is_available("ragg")) {
|
||||
if ((getOption('shiny.useragg') %OR% FALSE) && is_available("ragg")) {
|
||||
pngfun <- ragg::agg_png
|
||||
} else if (capabilities("aqua")) {
|
||||
# i.e., png(type = 'quartz')
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_available("Cairo")) {
|
||||
} else if ((getOption('shiny.usecairo') %OR% TRUE) && is_available("Cairo")) {
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
# i.e., png(type = 'cairo')
|
||||
@@ -24,7 +24,7 @@ startPNG <- function(filename, width, height, res, ...) {
|
||||
# to plot-time, but it shouldn't hurt to inform other the device directly as well
|
||||
if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
|
||||
# TODO: use :: once thematic is on CRAN
|
||||
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white", resolve = FALSE)
|
||||
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white")
|
||||
# auto vals aren't resolved until plot time, so if we see one, resolve it
|
||||
if (isTRUE("auto" == args$bg)) {
|
||||
args$bg <- getCurrentOutputInfo()[["bg"]]()
|
||||
|
||||
@@ -149,7 +149,7 @@ datePickerDependency <- function(theme) {
|
||||
})();
|
||||
</script>"
|
||||
),
|
||||
bslib::bs_dependency_defer(datePickerCSS)
|
||||
bootstraplib::bs_dependency_defer(datePickerCSS)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,11 +165,11 @@ datePickerCSS <- function(theme) {
|
||||
|
||||
scss_file <- system.file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
|
||||
|
||||
bslib::bs_dependency(
|
||||
bootstraplib::bs_dependency(
|
||||
input = sass::sass_file(scss_file),
|
||||
theme = theme,
|
||||
name = "bootstrap-datepicker",
|
||||
version = datePickerVersion,
|
||||
cache_key_extra = shinyPackageVersion()
|
||||
cache_key_extra = utils::packageVersion("shiny")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -233,54 +233,48 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
}
|
||||
|
||||
|
||||
selectizeDependency <- function() {
|
||||
bslib::bs_dependency_defer(selectizeDependencyFunc)
|
||||
selectizeVersion <- "0.12.4"
|
||||
|
||||
selectizeDependency <- function(theme) {
|
||||
list(
|
||||
htmlDependency(
|
||||
"selectize-js",
|
||||
selectizeVersion,
|
||||
src = c(href = "shared/selectize"),
|
||||
script = c(
|
||||
"js/selectize.min.js",
|
||||
# Accessibility plugin for screen readers (https://github.com/SLMNBJ/selectize-plugin-a11y):
|
||||
"accessibility/js/selectize-plugin-a11y.min.js"
|
||||
)
|
||||
),
|
||||
bootstraplib::bs_dependency_defer(selectizeCSS)
|
||||
)
|
||||
}
|
||||
|
||||
selectizeDependencyFunc <- function(theme) {
|
||||
selectizeVersion <- "0.12.4"
|
||||
selectizeCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(selectizeStaticDependency(selectizeVersion))
|
||||
return(htmlDependency(
|
||||
name = "selectize-css",
|
||||
version = selectizeVersion,
|
||||
src = c(href = "shared/selectize"),
|
||||
stylesheet = "css/selectize.bootstrap3.css"
|
||||
))
|
||||
}
|
||||
|
||||
selectizeDir <- system.file(package = "shiny", "www/shared/selectize/")
|
||||
stylesheet <- file.path(
|
||||
selectizeDir, "scss",
|
||||
if ("3" %in% bslib::theme_version(theme)) {
|
||||
scss_file <- system.file(
|
||||
package = "shiny", "www/shared/selectize/scss",
|
||||
if ("3" %in% bootstraplib::theme_version(theme)) {
|
||||
"selectize.bootstrap3.scss"
|
||||
} else {
|
||||
"selectize.bootstrap4.scss"
|
||||
}
|
||||
)
|
||||
# It'd be cleaner to ship the JS in a separate, href-based,
|
||||
# HTML dependency (which we currently do for other themable widgets),
|
||||
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
|
||||
# in HTML dependency named selectize, so if we were to change that
|
||||
# name, the JS/CSS would be loaded/included twice, which leads to
|
||||
# strange issues, especially since we now include a 3rd party
|
||||
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
|
||||
script <- file.path(
|
||||
selectizeDir, c("js/selectize.min.js", "accessibility/js/selectize-plugin-a11y.min.js")
|
||||
)
|
||||
bslib::bs_dependency(
|
||||
input = sass::sass_file(stylesheet),
|
||||
bootstraplib::bs_dependency(
|
||||
input = sass::sass_file(scss_file),
|
||||
theme = theme,
|
||||
name = "selectize",
|
||||
version = selectizeVersion,
|
||||
cache_key_extra = shinyPackageVersion(),
|
||||
.dep_args = list(script = script)
|
||||
)
|
||||
}
|
||||
|
||||
selectizeStaticDependency <- function(version) {
|
||||
htmlDependency(
|
||||
"selectize", version,
|
||||
src = c(href = "shared/selectize"),
|
||||
stylesheet = "css/selectize.bootstrap3.css",
|
||||
script = c(
|
||||
"js/selectize.min.js",
|
||||
"accessibility/js/selectize-plugin-a11y.min.js"
|
||||
)
|
||||
cache_key_extra = utils::packageVersion("shiny")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ ionRangeSliderDependency <- function() {
|
||||
src = c(href = "shared/strftime"),
|
||||
script = "strftime-min.js"
|
||||
),
|
||||
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
|
||||
bootstraplib::bs_dependency_defer(ionRangeSliderDependencyCSS)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -241,13 +241,8 @@ ionRangeSliderDependencyCSS <- function(theme) {
|
||||
# Remap some variable names for ionRangeSlider's scss
|
||||
sass_input <- list(
|
||||
list(
|
||||
# The bootswatch materia theme sets $input-bg: transparent;
|
||||
# which is an issue for the slider's handle(s) (#3130)
|
||||
bg = "if(alpha($input-bg)==0, $body-bg, $input-bg)",
|
||||
fg = sprintf(
|
||||
"if(alpha($input-color)==0, $%s, $input-color)",
|
||||
if ("3" %in% bslib::theme_version(theme)) "text-color" else "body-color"
|
||||
),
|
||||
bg = "$input-bg",
|
||||
fg = "$input-color",
|
||||
accent = "$component-active-bg",
|
||||
`font-family` = "$font-family-base"
|
||||
),
|
||||
@@ -256,12 +251,12 @@ ionRangeSliderDependencyCSS <- function(theme) {
|
||||
)
|
||||
)
|
||||
|
||||
bslib::bs_dependency(
|
||||
bootstraplib::bs_dependency(
|
||||
input = sass_input,
|
||||
theme = theme,
|
||||
name = "ionRangeSlider",
|
||||
version = ionRangeSliderVersion,
|
||||
cache_key_extra = shinyPackageVersion()
|
||||
cache_key_extra = utils::packageVersion("shiny")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ shiny_rmd_warning <- function() {
|
||||
|
||||
#' @rdname knitr_methods
|
||||
knit_print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %||% list()
|
||||
opts <- x$options %OR% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
|
||||
15
R/map.R
15
R/map.R
@@ -1,3 +1,18 @@
|
||||
# TESTS
|
||||
# Simple set/get
|
||||
# Simple remove
|
||||
# Simple containsKey
|
||||
# Simple keys
|
||||
# Simple values
|
||||
# Simple clear
|
||||
# Get of unknown key returns NULL
|
||||
# Remove of unknown key does nothing
|
||||
# Setting a key twice always results in last-one-wins
|
||||
# /TESTS
|
||||
|
||||
# Note that Map objects can't be saved in one R session and restored in
|
||||
# another, because they are based on fastmap, which uses an external pointer,
|
||||
# and external pointers can't be saved and restored in another session.
|
||||
#' @importFrom fastmap fastmap
|
||||
Map <- R6Class(
|
||||
'Map',
|
||||
|
||||
@@ -309,7 +309,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
createHttpuvApp = function() {
|
||||
list(
|
||||
onHeaders = function(req) {
|
||||
maxSize <- getOption('shiny.maxRequestSize') %||% (5 * 1024 * 1024)
|
||||
maxSize <- getOption('shiny.maxRequestSize') %OR% (5 * 1024 * 1024)
|
||||
if (maxSize <= 0)
|
||||
return(NULL)
|
||||
|
||||
@@ -346,7 +346,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
),
|
||||
catch = function(err) {
|
||||
httpResponse(status = 500L,
|
||||
content_type = "text/html; charset=UTF-8",
|
||||
content_type = "text/html",
|
||||
content = as.character(htmltools::htmlTemplate(
|
||||
system.file("template", "error.html", package = "shiny"),
|
||||
message = conditionMessage(err)
|
||||
|
||||
@@ -156,7 +156,6 @@ makeExtraMethods <- function() {
|
||||
"sendInsertUI",
|
||||
"sendModal",
|
||||
"setCurrentTheme",
|
||||
"getCurrentTheme",
|
||||
"sendNotification",
|
||||
"sendProgress",
|
||||
"sendRemoveTab",
|
||||
@@ -235,9 +234,9 @@ MockShinySession <- R6Class(
|
||||
progressStack = 'Stack',
|
||||
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
|
||||
token = 'character',
|
||||
#' @field cache The session cache object.
|
||||
#' @field cache The session cache MemoryCache.
|
||||
cache = NULL,
|
||||
#' @field appcache The app cache object.
|
||||
#' @field appcache The app cache MemoryCache.
|
||||
appcache = NULL,
|
||||
#' @field restoreContext Part of bookmarking support in a real
|
||||
#' `ShinySession` but always `NULL` for a `MockShinySession`.
|
||||
@@ -278,8 +277,8 @@ MockShinySession <- R6Class(
|
||||
# Copy app-level options
|
||||
self$options <- getCurrentAppState()$options
|
||||
|
||||
self$cache <- cachem::cache_mem()
|
||||
self$appcache <- cachem::cache_mem()
|
||||
self$cache <- MemoryCache$new()
|
||||
self$appcache <- MemoryCache$new()
|
||||
|
||||
# Adds various generated noop and error-producing method implementations.
|
||||
# Note that noop methods can be configured to produce warnings by setting
|
||||
|
||||
25
R/modules.R
25
R/modules.R
@@ -31,36 +31,11 @@ createSessionProxy <- function(parentSession, ...) {
|
||||
# but not `session$userData <- TRUE`) from within a module
|
||||
# without any hacks (see PR #1732)
|
||||
if (identical(x[[name]], value)) return(x)
|
||||
|
||||
# Special case for $options (issue #3112)
|
||||
if (name == "options") {
|
||||
session <- find_ancestor_session(x)
|
||||
session[[name]] <- value
|
||||
return(x)
|
||||
}
|
||||
|
||||
stop("Attempted to assign value on session proxy.")
|
||||
}
|
||||
|
||||
`[[<-.session_proxy` <- `$<-.session_proxy`
|
||||
|
||||
# Given a session_proxy, search `parent` recursively to find the real
|
||||
# ShinySession object. If given a ShinySession, simply return it.
|
||||
find_ancestor_session <- function(x, depth = 20) {
|
||||
if (depth < 0) {
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
if (inherits(x, "ShinySession")) {
|
||||
return(x)
|
||||
}
|
||||
if (inherits(x, "session_proxy")) {
|
||||
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
|
||||
}
|
||||
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
|
||||
|
||||
#' Shiny modules
|
||||
#'
|
||||
#' Shiny's module feature lets you break complicated UI and server logic into
|
||||
|
||||
185
R/reactives.R
185
R/reactives.R
@@ -947,7 +947,6 @@ Observable <- R6Class(
|
||||
#' @param domain See [domains].
|
||||
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
|
||||
#' [stacktrace()].
|
||||
#' @param ... Not used.
|
||||
#' @return a function, wrapped in a S3 class "reactive"
|
||||
#'
|
||||
#' @examples
|
||||
@@ -969,30 +968,20 @@ Observable <- R6Class(
|
||||
#' isolate(reactiveC())
|
||||
#' isolate(reactiveD())
|
||||
#' @export
|
||||
reactive <- function(x, env = parent.frame(), quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE)
|
||||
{
|
||||
check_dots_empty()
|
||||
|
||||
x <- get_quosure(x, env, quoted)
|
||||
fun <- as_function(x)
|
||||
# as_function returns a function that takes `...`. We need one that takes no
|
||||
# args.
|
||||
formals(fun) <- list()
|
||||
|
||||
reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE) {
|
||||
fun <- exprToFunction(x, env, quoted)
|
||||
# Attach a label and a reference to the original user source for debugging
|
||||
label <- exprToLabel(get_expr(x), "reactive", label)
|
||||
|
||||
srcref <- attr(substitute(x), "srcref", exact = TRUE)
|
||||
if (is.null(label)) {
|
||||
label <- rexprSrcrefToLabel(srcref[[1]],
|
||||
sprintf('reactive(%s)', paste(deparse(body(fun)), collapse='\n')))
|
||||
}
|
||||
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
|
||||
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
|
||||
o <- Observable$new(fun, label, domain, ..stacktraceon = ..stacktraceon)
|
||||
structure(
|
||||
o$getValue,
|
||||
observable = o,
|
||||
cacheHint = list(userExpr = zap_srcref(get_expr(x))),
|
||||
class = c("reactiveExpr", "reactive", "function")
|
||||
)
|
||||
structure(o$getValue, observable = o, class = c("reactiveExpr", "reactive", "function"))
|
||||
}
|
||||
|
||||
# Given the srcref to a reactive expression, attempts to figure out what the
|
||||
@@ -1064,14 +1053,6 @@ execCount <- function(x) {
|
||||
stop('Unexpected argument to execCount')
|
||||
}
|
||||
|
||||
# Internal utility functions for extracting things out of reactives.
|
||||
reactive_get_value_func <- function(x) {
|
||||
attr(x, "observable", exact = TRUE)$.origFunc
|
||||
}
|
||||
reactive_get_domain <- function(x) {
|
||||
attr(x, "observable", exact = TRUE)$.domain
|
||||
}
|
||||
|
||||
# Observer ------------------------------------------------------------------
|
||||
|
||||
Observer <- R6Class(
|
||||
@@ -1332,8 +1313,6 @@ Observer <- R6Class(
|
||||
#' automatically destroyed when its domain (if any) ends.
|
||||
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
|
||||
#' [stacktrace()].
|
||||
#' @param ... Not used.
|
||||
#'
|
||||
#' @return An observer reference class object. This object has the following
|
||||
#' methods:
|
||||
#' \describe{
|
||||
@@ -1388,36 +1367,18 @@ Observer <- R6Class(
|
||||
#' # are at the console, you can force a flush with flushReact()
|
||||
#' shiny:::flushReact()
|
||||
#' @export
|
||||
observe <- function(x, env = parent.frame(), quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
suspended = FALSE,
|
||||
priority = 0,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE)
|
||||
{
|
||||
check_dots_empty()
|
||||
observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
|
||||
suspended=FALSE, priority=0,
|
||||
domain=getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE) {
|
||||
|
||||
x <- get_quosure(x, env, quoted)
|
||||
fun <- as_function(x)
|
||||
# as_function returns a function that takes `...`. We need one that takes no
|
||||
# args.
|
||||
formals(fun) <- list()
|
||||
fun <- exprToFunction(x, env, quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('observe(%s)', paste(deparse(body(fun)), collapse='\n'))
|
||||
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('observe(%s)', paste(deparse(get_expr(x)), collapse='\n'))
|
||||
}
|
||||
|
||||
o <- Observer$new(
|
||||
fun,
|
||||
label = label,
|
||||
suspended = suspended,
|
||||
priority = priority,
|
||||
domain = domain,
|
||||
autoDestroy = autoDestroy,
|
||||
..stacktraceon = ..stacktraceon
|
||||
)
|
||||
o <- Observer$new(fun, label=label, suspended=suspended, priority=priority,
|
||||
domain=domain, autoDestroy=autoDestroy,
|
||||
..stacktraceon=..stacktraceon)
|
||||
invisible(o)
|
||||
}
|
||||
|
||||
@@ -1810,7 +1771,6 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
|
||||
rv <- reactiveValues(cookie = isolate(checkFunc()))
|
||||
|
||||
re_finalized <- FALSE
|
||||
env <- environment()
|
||||
|
||||
o <- observe({
|
||||
# When no one holds a reference to the reactive returned from
|
||||
@@ -1818,7 +1778,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
|
||||
# firing and hold onto resources.
|
||||
if (re_finalized) {
|
||||
o$destroy()
|
||||
rm(o, envir = env)
|
||||
rm(o, envir = parent.env(environment()))
|
||||
return()
|
||||
}
|
||||
|
||||
@@ -2161,7 +2121,6 @@ maskReactiveContext <- function(expr) {
|
||||
#' after the first time that the code in `handlerExpr` is run. This
|
||||
#' pattern is useful when you want to subscribe to a event that should only
|
||||
#' happen once.
|
||||
#' @param ... Currently not used.
|
||||
#'
|
||||
#' @return `observeEvent` returns an observer reference class object (see
|
||||
#' [observe()]). `eventReactive` returns a reactive expression
|
||||
@@ -2234,38 +2193,42 @@ maskReactiveContext <- function(expr) {
|
||||
observeEvent <- function(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)
|
||||
{
|
||||
check_dots_empty()
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE) {
|
||||
|
||||
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
|
||||
handlerExpr <- get_quosure(handlerExpr, handler.env, handler.quoted)
|
||||
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('observeEvent(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
|
||||
eventFunc <- wrapFunctionLabel(eventFunc, "observeEventExpr", ..stacktraceon = TRUE)
|
||||
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('observeEvent(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
|
||||
}
|
||||
handlerFunc <- exprToFunction(handlerExpr, handler.env, handler.quoted)
|
||||
handlerFunc <- wrapFunctionLabel(handlerFunc, "observeEventHandler", ..stacktraceon = TRUE)
|
||||
|
||||
handler <- inject(observe(
|
||||
!!handlerExpr,
|
||||
label = label,
|
||||
suspended = suspended,
|
||||
priority = priority,
|
||||
domain = domain,
|
||||
autoDestroy = TRUE,
|
||||
..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
|
||||
))
|
||||
initialized <- FALSE
|
||||
|
||||
o <- inject(bindEvent(
|
||||
ignoreNULL = ignoreNULL,
|
||||
ignoreInit = ignoreInit,
|
||||
once = once,
|
||||
label = label,
|
||||
!!eventExpr,
|
||||
x = handler
|
||||
))
|
||||
o <- observe({
|
||||
hybrid_chain(
|
||||
{eventFunc()},
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(o$destroy())
|
||||
}
|
||||
|
||||
isolate(handlerFunc())
|
||||
}
|
||||
)
|
||||
}, label = label, suspended = suspended, priority = priority, domain = domain,
|
||||
autoDestroy = TRUE, ..stacktraceon = FALSE)
|
||||
|
||||
invisible(o)
|
||||
}
|
||||
@@ -2275,26 +2238,34 @@ observeEvent <- function(eventExpr, handlerExpr,
|
||||
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)
|
||||
{
|
||||
check_dots_empty()
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
|
||||
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
|
||||
valueExpr <- get_quosure(valueExpr, value.env, value.quoted)
|
||||
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('eventReactive(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
|
||||
eventFunc <- wrapFunctionLabel(eventFunc, "eventReactiveExpr", ..stacktraceon = TRUE)
|
||||
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('eventReactive(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
|
||||
}
|
||||
handlerFunc <- exprToFunction(valueExpr, value.env, value.quoted)
|
||||
handlerFunc <- wrapFunctionLabel(handlerFunc, "eventReactiveHandler", ..stacktraceon = TRUE)
|
||||
|
||||
invisible(inject(bindEvent(
|
||||
ignoreNULL = ignoreNULL,
|
||||
ignoreInit = ignoreInit,
|
||||
label = label,
|
||||
!!eventExpr,
|
||||
x = reactive(!!valueExpr, domain = domain, label = label)
|
||||
)))
|
||||
initialized <- FALSE
|
||||
|
||||
invisible(reactive({
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(handlerFunc())
|
||||
}
|
||||
)
|
||||
}, label = label, domain = domain, ..stacktraceon = FALSE))
|
||||
}
|
||||
|
||||
isNullEvent <- function(value) {
|
||||
@@ -2454,7 +2425,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
|
||||
now <- getDomainTimeMs(domain)
|
||||
if (now >= v$when) {
|
||||
# Mod by 999999999 to get predictable overflow behavior
|
||||
v$trigger <- isolate(v$trigger %||% 0) %% 999999999 + 1
|
||||
v$trigger <- isolate(v$trigger %OR% 0) %% 999999999 + 1
|
||||
v$when <- NULL
|
||||
} else {
|
||||
invalidateLater(v$when - now)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
|
||||
#' this is a shortcut for using [bindCache()] with [renderPlot()].
|
||||
#' Renders a reactive plot, with plot images cached to disk.
|
||||
#'
|
||||
#' `expr` is an expression that generates a plot, similar to that in
|
||||
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
|
||||
@@ -41,6 +40,95 @@
|
||||
#' if there are multiple plots that have the same `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 `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 `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 `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 `"app"` or `"session"` is used, the cache will be 10 MB
|
||||
#' in size, and will be stored stored in memory, using a
|
||||
#' [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
|
||||
#' [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
|
||||
#' [shinyOptions()] at the top of your server function. To use
|
||||
#' the session-scoped cache, you must also call `renderCachedPlot` with
|
||||
#' `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 [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
|
||||
#' `myapp-cache` (replace `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 [memoryCache()] or [diskCache()], and pass it
|
||||
#' as the `cache` argument of `renderCachedPlot`.
|
||||
#'
|
||||
#' @section Interactive plots:
|
||||
#'
|
||||
#' `renderCachedPlot` can be used to create interactive plots. See
|
||||
@@ -48,7 +136,6 @@
|
||||
#'
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @inheritParams bindCache
|
||||
#' @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.
|
||||
@@ -59,13 +146,16 @@
|
||||
#' possible pixel dimension. See [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
|
||||
#' `"app"` (the default), `"session"`, or a cache object like
|
||||
#' a [diskCache()]. See the Cache Scoping section for more
|
||||
#' information.
|
||||
#' @param width,height not used. They are specified via the argument
|
||||
#' `sizePolicy`.
|
||||
#'
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of this
|
||||
#' function. It can be used with [bindCache()] to get the same effect as
|
||||
#' `renderCachedPlot()`. For more about configuring caches, see
|
||||
#' [cachem::cache_mem()] and [cachem::cache_disk()].
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of
|
||||
#' this function. For more about configuring caches, see
|
||||
#' [memoryCache()] and [diskCache()].
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
@@ -156,7 +246,7 @@
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = cachem::cache_mem()
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
@@ -165,7 +255,7 @@
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = cachem::cache_mem()
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
@@ -176,22 +266,22 @@
|
||||
#' # 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 = cachem::cache_mem(max_size = 20e6, max_age = 3600))
|
||||
#' 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 = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' 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 = cachem::cache_disk("./myapp-cache"))
|
||||
#' 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 = cachem::cache_mem(max_size = 5e6))
|
||||
#' shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
@@ -213,29 +303,275 @@ renderCachedPlot <- function(expr,
|
||||
height = NULL
|
||||
) {
|
||||
|
||||
expr <- substitute(expr)
|
||||
if (!is_quosure(expr)) {
|
||||
expr <- new_quosure(expr, env = parent.frame())
|
||||
}
|
||||
# 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())
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
if (!is_quosure(cacheKeyExpr)) {
|
||||
cacheKeyExpr <- new_quosure(cacheKeyExpr, env = parent.frame())
|
||||
}
|
||||
args <- list(...)
|
||||
|
||||
if (!is.null(width) || !is.null(height)) {
|
||||
warning("Unused argument(s) 'width' and/or 'height'. ",
|
||||
"'sizePolicy' is used instead.")
|
||||
}
|
||||
|
||||
inject(
|
||||
bindCache(
|
||||
renderPlot(!!expr, res = res, alt = alt, outputArgs = outputArgs, ...),
|
||||
!!cacheKeyExpr,
|
||||
sizePolicy = sizePolicy,
|
||||
cache = cache
|
||||
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)
|
||||
|
||||
# Make sure alt param to be reactive function
|
||||
if (is.reactive(alt))
|
||||
altWrapper <- alt
|
||||
else if (is.function(alt))
|
||||
altWrapper <- reactive({ alt() })
|
||||
else
|
||||
altWrapper <- function() { alt }
|
||||
|
||||
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
|
||||
# Make sure alt text to be reactive function
|
||||
alt <- altWrapper()
|
||||
})
|
||||
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = isolatedFunc,
|
||||
width = width,
|
||||
height = height,
|
||||
alt = alt,
|
||||
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
|
||||
alt <- altWrapper()
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
|
||||
|
||||
plotObj <- cache$get(key)
|
||||
|
||||
# First look in cache.
|
||||
# Case 1. cache hit.
|
||||
if (!is.key_missing(plotObj)) {
|
||||
return(list(
|
||||
cacheHit = TRUE,
|
||||
key = key,
|
||||
plotObj = plotObj,
|
||||
width = width,
|
||||
height = height,
|
||||
alt = alt,
|
||||
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,
|
||||
alt = alt,
|
||||
pixelratio = pixelratio
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
function(possiblyAsyncResult) {
|
||||
hybrid_chain(possiblyAsyncResult, function(result) {
|
||||
width <- result$width
|
||||
height <- result$height
|
||||
alt <- result$alt
|
||||
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,
|
||||
alt,
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -62,11 +62,9 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
execOnResize = FALSE, outputArgs = list()
|
||||
) {
|
||||
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
func <- quoToFunction(expr, "renderPlot", ..stacktraceon = TRUE)
|
||||
installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
|
||||
|
||||
args <- list(...)
|
||||
|
||||
@@ -91,9 +89,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
else
|
||||
altWrapper <- function() { alt }
|
||||
|
||||
# This is the function that will be used as getDims by default, but it can be
|
||||
# overridden (which happens when bindCache() is used).
|
||||
getDimsDefault <- function() {
|
||||
getDims <- function() {
|
||||
width <- widthWrapper()
|
||||
height <- heightWrapper()
|
||||
|
||||
@@ -112,7 +108,6 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
getDims <- NULL
|
||||
|
||||
# Calls drawPlot, invoking the user-provided `func` (which may or may not
|
||||
# return a promise). The idea is that the (cached) return value from this
|
||||
@@ -123,7 +118,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
{
|
||||
# If !execOnResize, don't invalidate when width/height changes.
|
||||
dims <- if (execOnResize) getDims() else isolate(getDims())
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
@@ -150,19 +145,15 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
# The `get_dims` parameter defaults to `getDimsDefault`. However, it can be
|
||||
# overridden, so that `bindCache` can use a different version.
|
||||
renderFunc <- function(shinysession, name, ..., get_dims = getDimsDefault) {
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
if (is.null(getDims)) getDims <<- get_dims
|
||||
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(result) {
|
||||
dims <- getDims()
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
result <- do.call("resizeSavedPlot", c(
|
||||
list(name, shinysession, result, dims$width, dims$height, altWrapper(), pixelratio, res),
|
||||
args
|
||||
@@ -180,14 +171,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
outputFunc <- plotOutput
|
||||
if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markedFunc <- markRenderFunction(
|
||||
outputFunc,
|
||||
renderFunc,
|
||||
outputArgs,
|
||||
cacheHint = list(userExpr = get_expr(expr), res = res)
|
||||
)
|
||||
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
|
||||
markedFunc
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
|
||||
@@ -253,9 +237,8 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
|
||||
promises::with_promise_domain(domain, {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value) {
|
||||
res <- withVisible(value)
|
||||
if (res$visible) {
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
# A modified version of print.ggplot which returns the built ggplot object
|
||||
# as well as the gtable grob. This overrides the ggplot::print.ggplot
|
||||
# method, but only within the context of renderPlot. The reason this needs
|
||||
@@ -273,7 +256,7 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
|
||||
# similar to ggplot2. But for base graphics, it would already have
|
||||
# been rendered when func was called above, and the print should
|
||||
# have no effect.
|
||||
result <- ..stacktraceon..(print(res$value))
|
||||
result <- ..stacktraceon..(print(value))
|
||||
# TODO jcheng 2017-04-11: Verify above ..stacktraceon..
|
||||
})
|
||||
result
|
||||
@@ -610,10 +593,6 @@ find_panel_info_api <- function(b) {
|
||||
coord <- ggplot2::summarise_coord(b)
|
||||
layers <- ggplot2::summarise_layers(b)
|
||||
|
||||
`%NA_OR%` <- function(x, y) {
|
||||
if (is_na(x)) y else x
|
||||
}
|
||||
|
||||
# Given x and y scale objects and a coord object, return a list that has
|
||||
# the bases of log transformations for x and y, or NULL if it's not a
|
||||
# log transform.
|
||||
@@ -630,8 +609,8 @@ find_panel_info_api <- function(b) {
|
||||
|
||||
# First look for log base in scale, then coord; otherwise NULL.
|
||||
list(
|
||||
x = get_log_base(xscale$trans) %NA_OR% coord$xlog %NA_OR% NULL,
|
||||
y = get_log_base(yscale$trans) %NA_OR% coord$ylog %NA_OR% NULL
|
||||
x = get_log_base(xscale$trans) %OR% coord$xlog %OR% NULL,
|
||||
y = get_log_base(yscale$trans) %OR% coord$ylog %OR% NULL
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,8 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
|
||||
rownames = FALSE, colnames = TRUE,
|
||||
digits = NULL, na = "NA", ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderTable")
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
if (!is.function(spacing)) spacing <- match.arg(spacing)
|
||||
|
||||
|
||||
@@ -142,8 +142,8 @@ runApp <- function(appDir=getwd(),
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache", default = NULL))) {
|
||||
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
|
||||
if (is.null(getShinyOption("cache"))) {
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
|
||||
@@ -80,7 +80,7 @@ addResourcePath <- function(prefix, directoryPath) {
|
||||
|
||||
# If a shiny app is currently running, dynamically register this path with
|
||||
# the corresponding httpuv server object.
|
||||
if (!is.null(getShinyOption("server", default = NULL)))
|
||||
if (!is.null(getShinyOption("server")))
|
||||
{
|
||||
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
|
||||
}
|
||||
|
||||
@@ -165,8 +165,7 @@ getShinyOption <- function(name, default = NULL) {
|
||||
#' `shinyOptions()`.
|
||||
#'
|
||||
#' \describe{ \item{cache}{A caching object that will be used by
|
||||
#' [renderCachedPlot()]. If not specified, a [cachem::cache_mem()] will be
|
||||
#' used.} }
|
||||
#' [renderCachedPlot()]. If not specified, a [memoryCache()] will be used.} }
|
||||
#'
|
||||
#' @param ... Options to set, with the form `name = value`.
|
||||
#' @aliases shiny-options
|
||||
|
||||
199
R/shiny.R
199
R/shiny.R
@@ -28,15 +28,6 @@ NULL
|
||||
#' @import methods
|
||||
NULL
|
||||
|
||||
#' @importFrom digest digest
|
||||
#' @importFrom promises promise promise_resolve promise_reject is.promising
|
||||
#' as.promise
|
||||
#' @importFrom rlang quo enquo as_function get_expr get_env new_function enquos
|
||||
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
|
||||
#' enquos0 zap_srcref %||% is_na
|
||||
#' @importFrom ellipsis check_dots_empty check_dots_unnamed
|
||||
NULL
|
||||
|
||||
createUniqueId <- function(bytes, prefix = "", suffix = "") {
|
||||
withPrivateSeed({
|
||||
paste(
|
||||
@@ -278,18 +269,6 @@ workerId <- local({
|
||||
#' character vector, as in `input=c("x", "y")`. The format can be
|
||||
#' "rds" or "json".
|
||||
#' }
|
||||
#' \item{setCurrentTheme(theme)}{
|
||||
#' Sets the current [bootstrapLib()] theme, which updates the value of
|
||||
#' [getCurrentTheme()], invalidates `session$getCurrentTheme()`, and calls
|
||||
#' function(s) registered with [registerThemeDependency()] with provided
|
||||
#' `theme`. If those function calls return [htmltools::htmlDependency()]s with
|
||||
#' `stylesheet`s, then those stylesheets are "refreshed" (i.e., the new
|
||||
#' stylesheets are inserted on the page and the old ones are disabled and
|
||||
#' removed).
|
||||
#' }
|
||||
#' \item{getCurrentTheme()}{
|
||||
#' A reactive read of the current [bootstrapLib()] theme.
|
||||
#' }
|
||||
#'
|
||||
#' @name session
|
||||
NULL
|
||||
@@ -381,7 +360,6 @@ ShinySession <- R6Class(
|
||||
currentOutputName = NULL, # Name of the currently-running output
|
||||
outputInfo = list(), # List of information for each output
|
||||
testSnapshotUrl = character(0),
|
||||
currentThemeDependency = NULL, # ReactiveVal for taking dependency on theme
|
||||
|
||||
sendResponse = function(requestMsg, value) {
|
||||
if (is.null(requestMsg$tag)) {
|
||||
@@ -487,7 +465,7 @@ ShinySession <- R6Class(
|
||||
# The format of the response that will be sent back. Defaults to
|
||||
# "json" unless requested otherwise. The only other valid value is
|
||||
# "rds".
|
||||
format <- params$format %||% "json"
|
||||
format <- params$format %OR% "json"
|
||||
|
||||
values <- list()
|
||||
|
||||
@@ -618,21 +596,21 @@ ShinySession <- R6Class(
|
||||
# function has been set, return the identity function.
|
||||
getSnapshotPreprocessOutput = function(name) {
|
||||
fun <- attr(private$.outputs[[name]], "snapshotPreprocess", exact = TRUE)
|
||||
fun %||% identity
|
||||
fun %OR% identity
|
||||
},
|
||||
|
||||
# Get the snapshotPreprocessInput function for an input name. If no preprocess
|
||||
# function has been set, return the identity function.
|
||||
getSnapshotPreprocessInput = function(name) {
|
||||
fun <- private$.input$getMeta(name, "shiny.snapshot.preprocess")
|
||||
fun %||% identity
|
||||
fun %OR% identity
|
||||
},
|
||||
|
||||
# See cycleStartAction
|
||||
startCycle = function() {
|
||||
# TODO: This should check for busyCount == 0L, and remove the checks from
|
||||
# the call sites
|
||||
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0) {
|
||||
if (length(private$cycleStartActionQueue) > 0) {
|
||||
head <- private$cycleStartActionQueue[[1L]]
|
||||
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
|
||||
|
||||
@@ -653,7 +631,13 @@ ShinySession <- R6Class(
|
||||
# busyCount, it's possible we're calling startCycle spuriously; that's
|
||||
# OK, it's essentially a no-op in that case.
|
||||
on.exit({
|
||||
later::later(private$startCycle)
|
||||
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0L) {
|
||||
later::later(function() {
|
||||
if (private$busyCount == 0L) {
|
||||
private$startCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, add = TRUE)
|
||||
|
||||
head()
|
||||
@@ -712,7 +696,7 @@ ShinySession <- R6Class(
|
||||
# Copy app-level options
|
||||
self$options <- getCurrentAppState()$options
|
||||
|
||||
self$cache <- cachem::cache_mem(max_size = 200 * 1024^2)
|
||||
self$cache <- MemoryCache$new()
|
||||
|
||||
private$bookmarkCallbacks <- Callbacks$new()
|
||||
private$bookmarkedCallbacks <- Callbacks$new()
|
||||
@@ -722,8 +706,6 @@ ShinySession <- R6Class(
|
||||
private$testMode <- getShinyOption("testmode", default = FALSE)
|
||||
private$enableTestSnapshot()
|
||||
|
||||
private$currentThemeDependency <- reactiveVal(0)
|
||||
|
||||
private$registerSessionEndCallbacks()
|
||||
|
||||
if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) {
|
||||
@@ -1298,11 +1280,6 @@ ShinySession <- R6Class(
|
||||
)
|
||||
},
|
||||
|
||||
getCurrentTheme = function() {
|
||||
private$currentThemeDependency()
|
||||
getShinyOption("bootstrapTheme")
|
||||
},
|
||||
|
||||
setCurrentTheme = function(theme) {
|
||||
# This function does three things: (1) sets theme as the current
|
||||
# bootstrapTheme, (2) re-executes any registered theme dependencies, and
|
||||
@@ -1311,11 +1288,8 @@ ShinySession <- R6Class(
|
||||
# Note that this will automatically scope to the session.
|
||||
shinyOptions(bootstrapTheme = theme)
|
||||
|
||||
# Invalidate
|
||||
private$currentThemeDependency(isolate(private$currentThemeDependency()) + 1)
|
||||
|
||||
# Call any theme dependency functions and make sure we get a list of deps back
|
||||
funcs <- getShinyOption("themeDependencyFuncs", default = list())
|
||||
funcs <- getShinyOption("themeDependencyFuncs")
|
||||
deps <- lapply(funcs, function(func) {
|
||||
deps <- func(theme)
|
||||
if (length(deps) == 0) return(NULL)
|
||||
@@ -1405,97 +1379,82 @@ ShinySession <- R6Class(
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
if (!is.null(private$outputInfo[[name]])) {
|
||||
return(private$outputInfo[[name]])
|
||||
}
|
||||
|
||||
# The following code will only run the first time this function has been
|
||||
# called for this output.
|
||||
|
||||
tmp_info <- list(name = name)
|
||||
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 <- isolate(names(self$clientData))
|
||||
.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.
|
||||
|
||||
# Note that all the following clientData values (which are reactiveValues)
|
||||
# are wrapped in reactive() so that users can take a dependency on particular
|
||||
# output info (i.e., just depend on width/height, or just depend on bg, fg, etc).
|
||||
# To put it another way, if getCurrentOutputInfo() simply returned a list of values
|
||||
# from self$clientData, than anything that calls getCurrentOutputInfo() would take
|
||||
# a reactive dependency on all of these values.
|
||||
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]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# parseCssColors() currently errors out if you hand it any NAs
|
||||
# This'll make sure we're always working with a string (and if
|
||||
# that string isn't a valid CSS color, will return NA)
|
||||
# https://github.com/rstudio/htmltools/issues/161
|
||||
parse_css_colors <- function(x) {
|
||||
htmltools::parseCssColors(x %||% "", mustWork = FALSE)
|
||||
htmltools::parseCssColors(x %OR% "", mustWork = FALSE)
|
||||
}
|
||||
|
||||
|
||||
# This function conditionally adds an item to tmp_info (for "width", it
|
||||
# would create tmp_info$width). It is added _if_ there is an entry in
|
||||
# clientData like "output_foo_width", where "foo" is the name of the
|
||||
# output. The first time `tmp_info$width()` is called, it creates a
|
||||
# reactive expression that reads `clientData$output_foo_width`, saves it,
|
||||
# then invokes that reactive. On subsequent calls, the reactive already
|
||||
# exists, so it simply invokes it.
|
||||
#
|
||||
# The reason it creates the reactive only on first use is so that it
|
||||
# doesn't spuriously create reactives.
|
||||
#
|
||||
# This function essentially generalizes the code below for names other
|
||||
# than just "width".
|
||||
#
|
||||
# width_name <- paste0("output_", name, "_width")
|
||||
# if (width_name %in% cd_names()) {
|
||||
# width_r <- NULL
|
||||
# tmp_info$width <- function() {
|
||||
# if (is.null(width_r)) {
|
||||
# width_r <<- reactive({
|
||||
# parse_css_colors(self$clientData[[width_name]])
|
||||
# })
|
||||
# }
|
||||
#
|
||||
# width_r()
|
||||
# }
|
||||
# }
|
||||
add_conditional_reactive <- function(prop, wrapfun = identity) {
|
||||
force(prop)
|
||||
force(wrapfun)
|
||||
|
||||
prop_name <- paste0("output_", name, "_", prop)
|
||||
|
||||
# Only add tmp_info$width if clientData has "output_foo_width"
|
||||
if (prop_name %in% cd_names) {
|
||||
r <- NULL
|
||||
|
||||
# Turn it into a function that creates a reactive on the first
|
||||
# invocation of getCurrentOutputInfo()$width() and saves it; future
|
||||
# invocations of getCurrentOutputInfo()$width() use the existing
|
||||
# reactive and save it.
|
||||
tmp_info[[prop]] <- function() {
|
||||
if (is.null(r)) {
|
||||
r <<- reactive(label = prop_name, {
|
||||
wrapfun(self$clientData[[prop_name]])
|
||||
})
|
||||
}
|
||||
|
||||
r()
|
||||
}
|
||||
}
|
||||
bg <- paste0("output_", name, "_bg")
|
||||
if (bg %in% cd_names()) {
|
||||
tmp_info$bg <- reactive({
|
||||
parse_css_colors(self$clientData[[bg]])
|
||||
})
|
||||
}
|
||||
|
||||
fg <- paste0("output_", name, "_fg")
|
||||
if (fg %in% cd_names()) {
|
||||
tmp_info$fg <- reactive({
|
||||
parse_css_colors(self$clientData[[fg]])
|
||||
})
|
||||
}
|
||||
|
||||
# Note that all the following clientData values (which are reactiveValues)
|
||||
# are wrapped in reactive() so that users can take a dependency on
|
||||
# particular output info (i.e., just depend on width/height, or just
|
||||
# depend on bg, fg, etc). To put it another way, if getCurrentOutputInfo()
|
||||
# simply returned a list of values from self$clientData, than anything
|
||||
# that calls getCurrentOutputInfo() would take a reactive dependency on
|
||||
# all of these values.
|
||||
add_conditional_reactive("width")
|
||||
add_conditional_reactive("height")
|
||||
add_conditional_reactive("bg", parse_css_colors)
|
||||
add_conditional_reactive("fg", parse_css_colors)
|
||||
add_conditional_reactive("accent", parse_css_colors)
|
||||
add_conditional_reactive("font")
|
||||
accent <- paste0("output_", name, "_accent")
|
||||
if (accent %in% cd_names()) {
|
||||
tmp_info$accent <- reactive({
|
||||
parse_css_colors(self$clientData[[accent]])
|
||||
})
|
||||
}
|
||||
|
||||
font <- paste0("output_", name, "_font")
|
||||
if (font %in% cd_names()) {
|
||||
tmp_info$font <- reactive({
|
||||
self$clientData[[font]]
|
||||
})
|
||||
}
|
||||
|
||||
private$outputInfo[[name]] <- tmp_info
|
||||
private$outputInfo[[name]]
|
||||
@@ -1512,7 +1471,7 @@ ShinySession <- R6Class(
|
||||
# Warn if trying to enable save-to-server bookmarking on a version of SS,
|
||||
# SSP, or Connect that doesn't support it.
|
||||
if (store == "server" && inShinyServer() &&
|
||||
is.null(getShinyOption("save.interface", default = NULL)))
|
||||
is.null(getShinyOption("save.interface")))
|
||||
{
|
||||
showNotification(
|
||||
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
|
||||
@@ -1928,17 +1887,15 @@ ShinySession <- R6Class(
|
||||
}
|
||||
return(httpResponse(
|
||||
200,
|
||||
download$contentType %||% getContentType(filename),
|
||||
download$contentType %OR% getContentType(filename),
|
||||
# owned=TRUE means tmpdata will be deleted after response completes
|
||||
list(file=tmpdata, owned=TRUE),
|
||||
c(
|
||||
'Content-Disposition' = ifelse(
|
||||
dlmatches[3] == '',
|
||||
paste0(
|
||||
'attachment; filename="',
|
||||
gsub('(["\\\\])', '\\\\\\1', filename),
|
||||
'"'
|
||||
),
|
||||
'attachment; filename="' %.%
|
||||
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
|
||||
'"',
|
||||
'attachment'
|
||||
),
|
||||
'Cache-Control'='no-cache')))
|
||||
|
||||
13
R/shinyapp.R
13
R/shinyapp.R
@@ -345,17 +345,6 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
|
||||
appDir <- findEnclosingApp(".")
|
||||
}
|
||||
|
||||
descFile <- file.path.ci(appDir, "DESCRIPTION")
|
||||
if (file.exists(file.path.ci(appDir, "NAMESPACE")) ||
|
||||
(file.exists(descFile) &&
|
||||
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")))
|
||||
{
|
||||
warning(
|
||||
"Loading R/ subdirectory for Shiny application, but this directory appears ",
|
||||
"to contain an R package. Sourcing files in R/ may cause unexpected behavior."
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(globalrenv)){
|
||||
# Evaluate global.R, if it exists.
|
||||
globalPath <- file.path.ci(appDir, "global.R")
|
||||
@@ -569,7 +558,7 @@ as.tags.shiny.appobj <- function(x, ...) {
|
||||
# jcheng 06/06/2014: Unfortunate copy/paste between this function and
|
||||
# knit_print.shiny.appobj, but I am trying to make the most conservative
|
||||
# change possible due to upcoming release.
|
||||
opts <- x$options %||% list()
|
||||
opts <- x$options %OR% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
|
||||
13
R/shinyui.R
13
R/shinyui.R
@@ -74,7 +74,7 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
if (testMode) {
|
||||
# Add code injection listener if in test mode
|
||||
shiny_deps[[length(shiny_deps) + 1]] <-
|
||||
htmlDependency("shiny-testmode", shinyPackageVersion(),
|
||||
htmlDependency("shiny-testmode", utils::packageVersion("shiny"),
|
||||
c(href="shared"), script = "shiny-testmode.js")
|
||||
}
|
||||
|
||||
@@ -83,11 +83,12 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
}
|
||||
|
||||
shinyDependencies <- function() {
|
||||
version <- utils::packageVersion("shiny")
|
||||
list(
|
||||
bslib::bs_dependency_defer(shinyDependencyCSS),
|
||||
bootstraplib::bs_dependency_defer(shinyDependencyCSS),
|
||||
htmlDependency(
|
||||
name = "shiny-javascript",
|
||||
version = shinyPackageVersion(),
|
||||
version = version,
|
||||
src = c(href = "shared"),
|
||||
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js"
|
||||
)
|
||||
@@ -95,7 +96,7 @@ shinyDependencies <- function() {
|
||||
}
|
||||
|
||||
shinyDependencyCSS <- function(theme) {
|
||||
version <- shinyPackageVersion()
|
||||
version <- utils::packageVersion("shiny")
|
||||
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
@@ -110,7 +111,7 @@ shinyDependencyCSS <- function(theme) {
|
||||
scss_files <- file.path(scss_home, c("bootstrap.scss", "shiny.scss"))
|
||||
scss_files <- lapply(scss_files, sass::sass_file)
|
||||
|
||||
bslib::bs_dependency(
|
||||
bootstraplib::bs_dependency(
|
||||
input = scss_files,
|
||||
theme = theme,
|
||||
name = "shiny-sass",
|
||||
@@ -142,7 +143,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
|
||||
|
||||
allowed_methods <- "GET"
|
||||
if (is.function(ui)) {
|
||||
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %||% allowed_methods
|
||||
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %OR% allowed_methods
|
||||
}
|
||||
|
||||
function(req) {
|
||||
|
||||
@@ -1,105 +1,34 @@
|
||||
utils::globalVariables('func', add = TRUE)
|
||||
utils::globalVariables('func')
|
||||
|
||||
#' Mark a function as a render function
|
||||
#'
|
||||
#' Should be called by implementers of `renderXXX` functions in order to mark
|
||||
#' their return values as Shiny render functions, and to provide a hint to Shiny
|
||||
#' regarding what UI function is most commonly used with this type of render
|
||||
#' function. This can be used in R Markdown documents to create complete output
|
||||
#' widgets out of just the render function.
|
||||
#' Should be called by implementers of `renderXXX` functions in order to
|
||||
#' mark their return values as Shiny render functions, and to provide a hint to
|
||||
#' Shiny regarding what UI function is most commonly used with this type of
|
||||
#' render function. This can be used in R Markdown documents to create complete
|
||||
#' output widgets out of just the render function.
|
||||
#'
|
||||
#' @param uiFunc A function that renders Shiny UI. Must take a single argument:
|
||||
#' an output ID.
|
||||
#' @param renderFunc A function that is suitable for assigning to a Shiny output
|
||||
#' slot.
|
||||
#' @param outputArgs A list of arguments to pass to the `uiFunc`. Render
|
||||
#' functions should include `outputArgs = list()` in their own parameter list,
|
||||
#' and pass through the value to `markRenderFunction`, to allow app authors to
|
||||
#' customize outputs. (Currently, this is only supported for dynamically
|
||||
#' generated UIs, such as those created by Shiny code snippets embedded in R
|
||||
#' Markdown documents).
|
||||
#' @param cacheHint One of `"auto"`, `FALSE`, or some other information to
|
||||
#' identify this instance for caching using [bindCache()]. If `"auto"`, it
|
||||
#' will try to automatically infer caching information. If `FALSE`, do not
|
||||
#' allow caching for the object. Some render functions (such as [renderPlot])
|
||||
#' contain internal state that makes them unsuitable for caching.
|
||||
#' @param cacheWriteHook Used if the render function is passed to `bindCache()`.
|
||||
#' This is an optional callback function to invoke before saving the value
|
||||
#' from the render function to the cache. This function must accept one
|
||||
#' argument, the value returned from `renderFunc`, and should return the value
|
||||
#' to store in the cache.
|
||||
#' @param cacheReadHook Used if the render function is passed to `bindCache()`.
|
||||
#' This is an optional callback function to invoke after reading a value from
|
||||
#' the cache (if there is a cache hit). The function will be passed one
|
||||
#' argument, the value retrieved from the cache. This can be useful when some
|
||||
#' side effect needs to occur for a render function to behave correctly. For
|
||||
#' example, some render functions call [createWebDependency()] so that Shiny
|
||||
#' is able to serve JS and CSS resources.
|
||||
#' functions should include `outputArgs = list()` in their own parameter
|
||||
#' list, and pass through the value to `markRenderFunction`, to allow
|
||||
#' app authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' @return The `renderFunc` function, with annotations.
|
||||
#'
|
||||
#' @seealso [createRenderFunction()], [quoToFunction()]
|
||||
#' @export
|
||||
markRenderFunction <- function(
|
||||
uiFunc,
|
||||
renderFunc,
|
||||
outputArgs = list(),
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
) {
|
||||
force(renderFunc)
|
||||
|
||||
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
|
||||
# a mutable object that keeps track of whether `useRenderFunction` has been
|
||||
# executed (this usually only happens when rendering Shiny code snippets in
|
||||
# an interactive R Markdown document); its initial value is FALSE
|
||||
hasExecuted <- Mutable$new()
|
||||
hasExecuted$set(FALSE)
|
||||
|
||||
if (is.null(uiFunc)) {
|
||||
uiFunc <- function(id) {
|
||||
pre(
|
||||
"No UI/output function provided for render function. ",
|
||||
"Please see ?shiny::markRenderFunction and ?shiny::createRenderFunction."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (identical(cacheHint, "auto")) {
|
||||
origUserFunc <- attr(renderFunc, "wrappedFunc", exact = TRUE)
|
||||
# The result could be NULL, but don't warn now because it'll only affect
|
||||
# users if they try to use caching. We'll warn when someone calls
|
||||
# bindCache() on this object.
|
||||
if (is.null(origUserFunc)) {
|
||||
cacheHint <- NULL
|
||||
} else {
|
||||
# Add in the wrapper render function and they output function, because
|
||||
# they can be useful for distinguishing two renderX functions that receive
|
||||
# the same user expression but do different things with them (like
|
||||
# renderText and renderPrint).
|
||||
cacheHint <- list(
|
||||
origUserFunc = origUserFunc,
|
||||
renderFunc = renderFunc,
|
||||
outputFunc = uiFunc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!is.null(cacheHint) && !is_false(cacheHint)) {
|
||||
if (!is.list(cacheHint)) {
|
||||
cacheHint <- list(cacheHint)
|
||||
}
|
||||
# For functions, remove the env and source refs because they can cause
|
||||
# spurious differences.
|
||||
# For expressions, remove source refs.
|
||||
# For everything else, do nothing.
|
||||
cacheHint <- lapply(cacheHint, function(x) {
|
||||
if (is.function(x)) formalsAndBody(x)
|
||||
else if (is.language(x)) zap_srcref(x)
|
||||
else x
|
||||
})
|
||||
}
|
||||
|
||||
wrappedRenderFunc <- function(...) {
|
||||
origRenderFunc <- renderFunc
|
||||
renderFunc <- function(...) {
|
||||
# if the user provided something through `outputArgs` BUT the
|
||||
# `useRenderFunction` was not executed, then outputArgs will be ignored,
|
||||
# so throw a warning to let user know the correct usage
|
||||
@@ -112,20 +41,15 @@ markRenderFunction <- function(
|
||||
# stop warning from happening again for the same object
|
||||
hasExecuted$set(TRUE)
|
||||
}
|
||||
if (is.null(formals(renderFunc))) renderFunc()
|
||||
else renderFunc(...)
|
||||
if (is.null(formals(origRenderFunc))) origRenderFunc()
|
||||
else origRenderFunc(...)
|
||||
}
|
||||
|
||||
structure(
|
||||
wrappedRenderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc,
|
||||
outputArgs = outputArgs,
|
||||
hasExecuted = hasExecuted,
|
||||
cacheHint = cacheHint,
|
||||
cacheWriteHook = cacheWriteHook,
|
||||
cacheReadHook = cacheReadHook
|
||||
)
|
||||
structure(renderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc,
|
||||
outputArgs = outputArgs,
|
||||
hasExecuted = hasExecuted)
|
||||
}
|
||||
|
||||
#' @export
|
||||
@@ -135,9 +59,6 @@ print.shiny.render.function <- function(x, ...) {
|
||||
|
||||
#' Implement render functions
|
||||
#'
|
||||
#' This function is a wrapper for [markRenderFunction()] which provides support
|
||||
#' for async computation via promises.
|
||||
#'
|
||||
#' @param func A function without parameters, that returns user data. If the
|
||||
#' returned value is a promise, then the render function will proceed in async
|
||||
#' mode.
|
||||
@@ -149,63 +70,34 @@ print.shiny.render.function <- function(x, ...) {
|
||||
#' @param outputFunc The UI function that is used (or most commonly used) with
|
||||
#' this render function. This can be used in R Markdown documents to create
|
||||
#' complete output widgets out of just the render function.
|
||||
#' @inheritParams markRenderFunction
|
||||
#' @param outputArgs A list of arguments to pass to the `outputFunc`.
|
||||
#' Render functions should include `outputArgs = list()` in their own
|
||||
#' parameter list, and pass through the value as this argument, to allow app
|
||||
#' authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' @return An annotated render function, ready to be assigned to an
|
||||
#' `output` slot.
|
||||
#'
|
||||
#' @seealso [quoToFunction()], [markRenderFunction()].
|
||||
#'
|
||||
#' @examples
|
||||
#' # A very simple render function
|
||||
#' renderTriple <- function(x) {
|
||||
#' x <- substitute(x)
|
||||
#' if (!rlang::is_quosure(x)) {
|
||||
#' x <- rlang::new_quosure(x, env = parent.frame())
|
||||
#' }
|
||||
#' func <- quoToFunction(x, "renderTriple")
|
||||
#'
|
||||
#' createRenderFunction(
|
||||
#' func,
|
||||
#' transform = function(value, session, name, ...) {
|
||||
#' paste(rep(value, 3), collapse=", ")
|
||||
#' },
|
||||
#' outputFunc = textOutput
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Test render function from the console
|
||||
#' a <- 1
|
||||
#' r <- renderTriple({ a + 1 })
|
||||
#' a <- 2
|
||||
#' r()
|
||||
#' @export
|
||||
createRenderFunction <- function(
|
||||
func,
|
||||
transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL,
|
||||
outputArgs = NULL,
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
func, transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL, outputArgs = NULL
|
||||
) {
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value) {
|
||||
transform(value, shinysession, name, ...)
|
||||
function(value, .visible) {
|
||||
transform(setVisible(value, .visible), shinysession, name, ...)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Hoist func's wrappedFunc attribute into renderFunc, so that when we pass
|
||||
# renderFunc on to markRenderFunction, it is able to find the original user
|
||||
# function.
|
||||
if (identical(cacheHint, "auto")) {
|
||||
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
|
||||
}
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
|
||||
cacheWriteHook, cacheReadHook)
|
||||
if (!is.null(outputFunc))
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
else
|
||||
renderFunc
|
||||
}
|
||||
|
||||
useRenderFunction <- function(renderFunc, inline = FALSE) {
|
||||
@@ -248,22 +140,6 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
useRenderFunction(x, inline = inline)
|
||||
}
|
||||
|
||||
# Get relevant attributes from a render function object.
|
||||
renderFunctionAttributes <- function(x) {
|
||||
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint")
|
||||
names(attrs) <- attrs
|
||||
lapply(attrs, function(name) attr(x, name, exact = TRUE))
|
||||
}
|
||||
|
||||
# Add a named list of attributes to an object
|
||||
addAttributes <- function(x, attrs) {
|
||||
nms <- names(attrs)
|
||||
for (i in seq_along(attrs)) {
|
||||
attr(x, nms[i]) <- attrs[[i]]
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
|
||||
#' Mark a render function with attributes that will be used by the output
|
||||
#'
|
||||
@@ -398,10 +274,8 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
deleteFile, outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderImage")
|
||||
deleteFile, outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
# missing() must be used directly within the function with the given arg
|
||||
if (missing(deleteFile)) {
|
||||
@@ -450,7 +324,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
}
|
||||
|
||||
# If contentType not specified, autodetect based on extension
|
||||
contentType <- imageinfo$contentType %||% getContentType(imageinfo$src)
|
||||
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
|
||||
|
||||
# Extra values are everything in imageinfo except 'src' and 'contentType'
|
||||
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
|
||||
@@ -459,10 +333,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
},
|
||||
imageOutput,
|
||||
outputArgs,
|
||||
cacheHint = FALSE
|
||||
)
|
||||
imageOutput, outputArgs)
|
||||
}
|
||||
|
||||
# TODO: If we ever take a dependency on fs, it'd be great to replace this with
|
||||
@@ -534,10 +405,8 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
|
||||
#' @example res/text-example.R
|
||||
#' @export
|
||||
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
width = getOption('width'), outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderPrint")
|
||||
width = getOption('width'), outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
# Set a promise domain that sets the console width
|
||||
# and captures output
|
||||
@@ -550,12 +419,12 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
{
|
||||
promises::with_promise_domain(domain, func())
|
||||
},
|
||||
function(value) {
|
||||
res <- withVisible(value)
|
||||
if (res$visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(res$value, append = TRUE), collapse = "\n"))
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
|
||||
}
|
||||
paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
res
|
||||
},
|
||||
finally = function() {
|
||||
close(domain$conn)
|
||||
@@ -563,15 +432,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
)
|
||||
}
|
||||
|
||||
markRenderFunction(
|
||||
verbatimTextOutput,
|
||||
renderFunc,
|
||||
outputArgs,
|
||||
cacheHint = list(
|
||||
label = "renderPrint",
|
||||
origUserExpr = get_expr(expr)
|
||||
)
|
||||
)
|
||||
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
createRenderPrintPromiseDomain <- function(width) {
|
||||
@@ -621,17 +482,14 @@ createRenderPrintPromiseDomain <- function(width) {
|
||||
#' @rdname renderPrint
|
||||
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list(), sep=" ") {
|
||||
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderText")
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(value, session, name, ...) {
|
||||
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
|
||||
},
|
||||
textOutput,
|
||||
outputArgs
|
||||
textOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -672,11 +530,9 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
outputArgs = list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderUI")
|
||||
renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
@@ -686,8 +542,7 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
|
||||
processDeps(result, shinysession)
|
||||
},
|
||||
uiOutput,
|
||||
outputArgs
|
||||
uiOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -748,7 +603,7 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
|
||||
shinysession$registerDownload(name, filename, contentType, content)
|
||||
}
|
||||
snapshotExclude(
|
||||
markRenderFunction(downloadButton, renderFunc, outputArgs, cacheHint = FALSE)
|
||||
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -819,10 +674,8 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
|
||||
renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
callback = 'function(oTable) {}', escape = TRUE,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderDataTable")
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
if (is.function(options)) options <- options()
|
||||
@@ -856,8 +709,7 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs,
|
||||
cacheHint = FALSE)
|
||||
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
|
||||
|
||||
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
|
||||
# Remove the action field so that it's not saved in test snapshots. It
|
||||
|
||||
110
R/utils-lang.R
110
R/utils-lang.R
@@ -1,110 +0,0 @@
|
||||
# Given a list of quosures, return a function that will evaluate them and return
|
||||
# a list of resulting values. If the list contains a single expression, unwrap
|
||||
# it from the list.
|
||||
quos_to_func <- function(qs) {
|
||||
if (length(qs) == 0) {
|
||||
stop("Need at least one item in `...` to use as cache key or event.")
|
||||
}
|
||||
|
||||
if (length(qs) == 1) {
|
||||
# Special case for one quosure. This is needed for async to work -- that is,
|
||||
# when the quosure returns a promise. It needs to not be wrapped into a list
|
||||
# for the hybrid_chain stuff to detect that it's a promise. (Plus, it's not
|
||||
# even clear what it would mean to mix promises and non-promises in the
|
||||
# key.)
|
||||
qs <- qs[[1]]
|
||||
function() {
|
||||
eval_tidy(qs)
|
||||
}
|
||||
|
||||
} else {
|
||||
function() {
|
||||
lapply(qs, eval_tidy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Given a list of quosures, return a string representation of the expressions.
|
||||
#
|
||||
# qs <- list(quo(a+1), quo({ b+2; b + 3 }))
|
||||
# quos_to_label(qs)
|
||||
# #> [1] "a + 1, {\n b + 2\n b + 3\n}"
|
||||
quos_to_label <- function(qs) {
|
||||
res <- lapply(qs, function(q) {
|
||||
paste(deparse(get_expr(q)), collapse = "\n")
|
||||
})
|
||||
|
||||
paste(res, collapse = ", ")
|
||||
}
|
||||
|
||||
# Get the formals and body for a function, without source refs. This is used for
|
||||
# consistent hashing of the function.
|
||||
formalsAndBody <- function(x) {
|
||||
if (is.null(x)) {
|
||||
return(list())
|
||||
}
|
||||
|
||||
list(
|
||||
formals = formals(x),
|
||||
body = body(zap_srcref(x))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# This function is to be called from functions like `reactive()`, `observe()`,
|
||||
# and the various render functions. It handles the following cases:
|
||||
# - The typical case where x is an unquoted expression, and `env` and `quoted`
|
||||
# are not used.
|
||||
# - New-style metaprogramming cases, where rlang::inject() is used to inline a
|
||||
# quosure into the AST, as in `inject(reactive(!!x))`.
|
||||
# - Old-style metaprogramming cases, where `env` and/or `quoted` are used.
|
||||
#
|
||||
# Much of the complexity is handling old-style metaprogramming cases. The code
|
||||
# in this function is more complicated because it needs to look at unevaluated
|
||||
# expressions in the _calling_ function. If this code were put directly in the
|
||||
# calling function, it would look like this:
|
||||
#
|
||||
# if (!missing(env) || !missing(quoted)) {
|
||||
# deprecatedEnvQuotedMessage()
|
||||
# if (!quoted) x <- substitute(x)
|
||||
# x <- new_quosure(x, env)
|
||||
#
|
||||
# } else {
|
||||
# x <- substitute(x)
|
||||
# if (!is_quosure(x)) {
|
||||
# x <- new_quosure(x, env = parent.frame())
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# In the future, the calling functions will not need to have the `env` and
|
||||
# `quoted` arguments -- `rlang::inject()` and quosures can be used instead.
|
||||
# Instead of using this function, `get_quosure()`, the caller can instead use
|
||||
# just the following code:
|
||||
#
|
||||
# x <- substitute(x)
|
||||
# if (!is_quosure(x)) {
|
||||
# x <- new_quosure(x, env = parent.frame())
|
||||
# }
|
||||
#
|
||||
get_quosure <- function(x, env, quoted) {
|
||||
if (!eval(substitute(missing(env)), parent.frame()) ||
|
||||
!eval(substitute(missing(quoted)), parent.frame()))
|
||||
{
|
||||
deprecatedEnvQuotedMessage()
|
||||
if (!quoted) {
|
||||
x <- eval(substitute(substitute(x)), parent.frame())
|
||||
}
|
||||
x <- new_quosure(x, env)
|
||||
|
||||
} else {
|
||||
x <- eval(substitute(substitute(x)), parent.frame())
|
||||
|
||||
# At this point, x can be a quosure if rlang::inject() is used, but the
|
||||
# typical case is that x is not a quosure.
|
||||
if (!is_quosure(x)) {
|
||||
x <- new_quosure(x, env = parent.frame(2L))
|
||||
}
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
195
R/utils.R
195
R/utils.R
@@ -113,6 +113,24 @@ isWholeNum <- function(x, tol = .Machine$double.eps^0.5) {
|
||||
abs(x - round(x)) < tol
|
||||
}
|
||||
|
||||
`%OR%` <- function(x, y) {
|
||||
if (is.null(x) || isTRUE(is.na(x)))
|
||||
y
|
||||
else
|
||||
x
|
||||
}
|
||||
|
||||
`%AND%` <- function(x, y) {
|
||||
if (!is.null(x) && !isTRUE(is.na(x)))
|
||||
if (!is.null(y) && !isTRUE(is.na(y)))
|
||||
return(y)
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
`%.%` <- function(x, y) {
|
||||
paste(x, y, sep='')
|
||||
}
|
||||
|
||||
# Given a vector or list, drop all the NULL items in it
|
||||
dropNulls <- function(x) {
|
||||
x[!vapply(x, is.null, FUN.VALUE=logical(1))]
|
||||
@@ -202,11 +220,6 @@ sort_c <- function(x, ...) {
|
||||
sort(x, method = "radix", ...)
|
||||
}
|
||||
|
||||
# Base R isFALSE function was added in R 3.5.0.
|
||||
is_false <- function(x) {
|
||||
identical(x, FALSE)
|
||||
}
|
||||
|
||||
# Wrapper around list2env with a NULL check. In R <3.2.0, if an empty unnamed
|
||||
# list is passed to list2env(), it errors. But an empty named list is OK. For
|
||||
# R >=3.2.0, this wrapper is not necessary.
|
||||
@@ -414,8 +427,7 @@ makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
|
||||
#' Convert an expression to a function
|
||||
#'
|
||||
#' This is to be called from another function, because it will attempt to get
|
||||
#' an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
|
||||
#' recommended to use [quoToFunction()] instead.
|
||||
#' an unquoted expression from two calls back.
|
||||
#'
|
||||
#' If expr is a quoted expression, then this just converts it to a function.
|
||||
#' If expr is a function, then this simply returns expr (and prints a
|
||||
@@ -473,8 +485,7 @@ exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
|
||||
#' Install an expression as a function
|
||||
#'
|
||||
#' Installs an expression in the given environment as a function, and registers
|
||||
#' debug hooks so that breakpoints may be set in the function. Note: as of
|
||||
#' Shiny 1.6.0, it is recommended to use [quoToFunction()] instead.
|
||||
#' debug hooks so that breakpoints may be set in the function.
|
||||
#'
|
||||
#' This function can replace `exprToFunction` as follows: we may use
|
||||
#' `func <- exprToFunction(expr)` if we do not want the debug hooks, or
|
||||
@@ -520,48 +531,6 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
|
||||
assign(name, func, envir = assign.env)
|
||||
}
|
||||
|
||||
#' Convert a quosure to a function for a Shiny render function
|
||||
#'
|
||||
#' This takes a quosure and label, and wraps them into a function that should be
|
||||
#' passed to [createRenderFunction()] or [markRenderFunction()].
|
||||
#'
|
||||
#' This function was added in Shiny 1.6.0. Previously, it was recommended to use
|
||||
#' [installExprFunction()] or [exprToFunction()] in render functions, but now we
|
||||
#' recommend using [quoToFunction()], because it does not require `env` and
|
||||
#' `quoted` arguments -- that information is captured by quosures provided by
|
||||
#' \pkg{rlang}.
|
||||
#'
|
||||
#' @param q A quosure.
|
||||
#' @inheritParams installExprFunction
|
||||
#' @seealso [createRenderFunction()] for example usage.
|
||||
#'
|
||||
#' @export
|
||||
quoToFunction <- function(q, label, ..stacktraceon = FALSE) {
|
||||
q <- as_quosure(q)
|
||||
# Use new_function() instead of as_function(), because as_function() adds an
|
||||
# extra parent environment. (This may not actually be a problem, though.)
|
||||
func <- new_function(NULL, get_expr(q), get_env(q))
|
||||
wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
|
||||
}
|
||||
|
||||
|
||||
# Utility function for creating a debugging label, given an expression.
|
||||
# `expr` is a quoted expression.
|
||||
# `function_name` is the name of the calling function.
|
||||
# `label` is an optional user-provided label. If NULL, it will be inferred.
|
||||
exprToLabel <- function(expr, function_name, label = NULL) {
|
||||
srcref <- attr(expr, "srcref", exact = TRUE)
|
||||
if (is.null(label)) {
|
||||
label <- rexprSrcrefToLabel(
|
||||
srcref[[1]],
|
||||
sprintf('%s(%s)', function_name, paste(deparse(expr), collapse = '\n'))
|
||||
)
|
||||
}
|
||||
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
|
||||
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
|
||||
label
|
||||
}
|
||||
|
||||
#' Parse a GET query string from a URL
|
||||
#'
|
||||
#' Returns a named list of key-value pairs.
|
||||
@@ -662,6 +631,37 @@ shinyCallingHandlers <- function(expr) {
|
||||
)
|
||||
}
|
||||
|
||||
#' Print message for deprecated functions in Shiny
|
||||
#'
|
||||
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
|
||||
#'
|
||||
#' @param new Name of replacement function.
|
||||
#' @param msg Message to print. If used, this will override the default message.
|
||||
#' @param old Name of deprecated function.
|
||||
#' @param version The last version of Shiny before the item was deprecated.
|
||||
#' @keywords internal
|
||||
shinyDeprecated <- function(new=NULL, msg=NULL,
|
||||
old=as.character(sys.call(sys.parent()))[1L],
|
||||
version = NULL) {
|
||||
|
||||
if (getOption("shiny.deprecation.messages") %OR% TRUE == FALSE)
|
||||
return(invisible())
|
||||
|
||||
if (is.null(msg)) {
|
||||
msg <- paste(old, "is deprecated.")
|
||||
if (!is.null(new)) {
|
||||
msg <- paste(msg, "Please use", new, "instead.",
|
||||
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
|
||||
}
|
||||
}
|
||||
|
||||
if (!is.null(version)) {
|
||||
msg <- paste0(msg, " (Last used in version ", version, ")")
|
||||
}
|
||||
|
||||
# Similar to .Deprecated(), but print a message instead of warning
|
||||
message(msg)
|
||||
}
|
||||
|
||||
#' Register a function with the debugger (if one is active).
|
||||
#'
|
||||
@@ -1587,19 +1587,14 @@ URLencode <- function(value, reserved = FALSE) {
|
||||
if (reserved) encodeURIComponent(value) else encodeURI(value)
|
||||
}
|
||||
|
||||
# Make sure user-supplied dates are either NULL or can be coerced to a
|
||||
# yyyy-mm-dd formatted string. If a date is specified, this function returns a
|
||||
# string for consistency across locales. Also, `as.Date()` is used to coerce
|
||||
# strings to date objects so that strings like "2016-08-9" are expanded to
|
||||
# "2016-08-09". If any of the values result in error or NA, then the input
|
||||
# `date` is returned unchanged.
|
||||
# Make user-supplied dates are either NULL or can be coerced
|
||||
# to a yyyy-mm-dd formatted string. If a date is specified, this
|
||||
# function returns a string for consistency across locales.
|
||||
# Also, `as.Date()` is used to coerce strings to date objects
|
||||
# so that strings like "2016-08-9" are expanded to "2016-08-09"
|
||||
dateYMD <- function(date = NULL, argName = "value") {
|
||||
if (!length(date)) return(NULL)
|
||||
tryCatch({
|
||||
res <- format(as.Date(date), "%Y-%m-%d")
|
||||
if (any(is.na(res))) stop()
|
||||
date <- res
|
||||
},
|
||||
tryCatch(date <- format(as.Date(date), "%Y-%m-%d"),
|
||||
error = function(e) {
|
||||
warning(
|
||||
"Couldn't coerce the `", argName,
|
||||
@@ -1622,17 +1617,18 @@ wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
|
||||
assign(name, func, environment())
|
||||
registerDebugHook(name, environment(), name)
|
||||
|
||||
if (..stacktraceon) {
|
||||
# We need to wrap the `...` in `!!quote(...)` so that R CMD check won't
|
||||
# complain about "... may be used in an incorrect context"
|
||||
body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
|
||||
} else {
|
||||
body <- expr({ (!!name)(!!quote(...)) })
|
||||
}
|
||||
relabelWrapper <- new_function(pairlist2(... =), body, environment())
|
||||
relabelWrapper <- eval(substitute(
|
||||
function(...) {
|
||||
# This `f` gets renamed to the value of `name`. Note that it may not
|
||||
# print as the new name, because of source refs stored in the function.
|
||||
if (..stacktraceon)
|
||||
..stacktraceon..(f(...))
|
||||
else
|
||||
f(...)
|
||||
},
|
||||
list(f = as.name(name))
|
||||
))
|
||||
|
||||
# Preserve the original function that was passed in; is used for caching.
|
||||
attr(relabelWrapper, "wrappedFunc") <- func
|
||||
relabelWrapper
|
||||
}
|
||||
|
||||
@@ -1692,23 +1688,19 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
|
||||
if (promises::is.promising(result$value)) {
|
||||
# Purposefully NOT including domain (nor replace), as we're already in
|
||||
# the domain at this point
|
||||
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
|
||||
p <- promise_chain(setVisible(result), ..., catch = catch, finally = finally)
|
||||
runFinally <- FALSE
|
||||
p
|
||||
} else {
|
||||
result <- Reduce(
|
||||
function(v, func) {
|
||||
if (v$visible) {
|
||||
withVisible(func(v$value))
|
||||
} else {
|
||||
withVisible(func(invisible(v$value)))
|
||||
}
|
||||
},
|
||||
list(...),
|
||||
result
|
||||
)
|
||||
result <- Reduce(function(v, func) {
|
||||
if (".visible" %in% names(formals(func))) {
|
||||
withVisible(func(v$value, .visible = v$visible))
|
||||
} else {
|
||||
withVisible(func(v$value))
|
||||
}
|
||||
}, list(...), result)
|
||||
|
||||
valueWithVisible(result)
|
||||
setVisible(result)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -1729,12 +1721,23 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
|
||||
}
|
||||
}
|
||||
|
||||
# Given a list with items named `value` and `visible`, return `x$value` either
|
||||
# visibly, or invisibly, depending on the value of `x$visible`.
|
||||
valueWithVisible <- function(x) {
|
||||
if (x$visible) x$value else invisible(x$value)
|
||||
}
|
||||
# Returns `value` with either `invisible()` applied or not, depending on the
|
||||
# value of `visible`.
|
||||
#
|
||||
# If the `visible` is missing, then `value` should be a list as returned from
|
||||
# `withVisible()`, and that visibility will be applied.
|
||||
setVisible <- function(value, visible) {
|
||||
if (missing(visible)) {
|
||||
visible <- value$visible
|
||||
value <- value$value
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
invisible(value)
|
||||
} else {
|
||||
(value)
|
||||
}
|
||||
}
|
||||
|
||||
createVarPromiseDomain <- function(env, name, value) {
|
||||
force(env)
|
||||
@@ -1889,15 +1892,3 @@ is_available <- function(package, version = NULL) {
|
||||
}
|
||||
installed && isTRUE(utils::packageVersion(package) >= version)
|
||||
}
|
||||
|
||||
|
||||
# cached version of utils::packageVersion("shiny")
|
||||
shinyPackageVersion <- local({
|
||||
version <- NULL
|
||||
function() {
|
||||
if (is.null(version)) {
|
||||
version <<- utils::packageVersion("shiny")
|
||||
}
|
||||
version
|
||||
}
|
||||
})
|
||||
|
||||
12
README.md
12
README.md
@@ -46,6 +46,18 @@ remotes::install_github("rstudio/shiny")
|
||||
|
||||
To learn more we highly recommend you check out the [Shiny Tutorial](http://shiny.rstudio.com/tutorial/). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.
|
||||
|
||||
## Bootstrap 3 migration
|
||||
|
||||
Shiny versions 0.10.2.2 and below used the Bootstrap 2 web framework. After 0.10.2.2, Shiny switched to Bootstrap 3. For most users, the upgrade should be seamless. However, if you have have customized your HTML-generating code to use features specific to Bootstrap 2, you may need to update your code to work with Bootstrap 3.
|
||||
|
||||
If you do not wish to update your code at this time, you can use the [shinybootstrap2](https://github.com/rstudio/shinybootstrap2) package for backward compatibility.
|
||||
|
||||
If you prefer to install an older version of Shiny, you can do it using the devtools package:
|
||||
|
||||
```R
|
||||
devtools::install_version("shiny", version = "0.10.2.2")
|
||||
```
|
||||
|
||||
## Development notes
|
||||
|
||||
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
library(shinytest)
|
||||
expect_pass(testApp("../", suffix = osName()))
|
||||
shinytest::testApp("../")
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.day:hover, .datepicker table tr td.focused {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background: #e9e9ea;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -121,37 +121,40 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #83ccd9;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted:focus, .datepicker table tr td.highlighted.focus {
|
||||
color: #000;
|
||||
background-color: #bcd4d9;
|
||||
border-color: #6299a3;
|
||||
color: #212529;
|
||||
background-color: #bfd8dd;
|
||||
border-color: #6aa2ad;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted:hover {
|
||||
color: #000;
|
||||
background-color: #697679;
|
||||
border-color: #73b3bf;
|
||||
color: #212529;
|
||||
background-color: #79898d;
|
||||
border-color: #77b8c4;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted:active, .datepicker table tr td.highlighted.active {
|
||||
color: #000;
|
||||
background-color: #bcd4d9;
|
||||
border-color: #73b3bf;
|
||||
color: #212529;
|
||||
background-color: #bfd8dd;
|
||||
border-color: #77b8c4;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted.focus:active, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
|
||||
color: #000;
|
||||
background-color: #adc4c8;
|
||||
border-color: #6299a3;
|
||||
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted:active.focus, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
|
||||
color: #212529;
|
||||
background-color: #b3cacf;
|
||||
border-color: #6aa2ad;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
|
||||
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted:hover,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted:focus,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted.focus {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #83ccd9;
|
||||
}
|
||||
@@ -166,36 +169,39 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.today {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today:focus, .datepicker table tr td.today.focus {
|
||||
color: #000;
|
||||
background-color: #e6c58a;
|
||||
border-color: #bf8926;
|
||||
color: #212529;
|
||||
background-color: #e9c98e;
|
||||
border-color: #c89331;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today:hover {
|
||||
color: #000;
|
||||
background-color: #806e4d;
|
||||
border-color: #e0a12d;
|
||||
color: #212529;
|
||||
background-color: #908061;
|
||||
border-color: #e4a532;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today:active, .datepicker table tr td.today.active {
|
||||
color: #000;
|
||||
background-color: #e6c58a;
|
||||
border-color: #e0a12d;
|
||||
color: #212529;
|
||||
background-color: #e9c98e;
|
||||
border-color: #e4a532;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today.focus:active, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
|
||||
color: #000;
|
||||
background-color: #d4b67f;
|
||||
border-color: #bf8926;
|
||||
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today:active.focus, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
|
||||
color: #212529;
|
||||
background-color: #d9bc86;
|
||||
border-color: #c89331;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today.focus[disabled], fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
|
||||
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today.focus {
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
@@ -210,37 +216,40 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.range {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background-color: #e9e9ea;
|
||||
border-color: #b5b5b8;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range:focus, .datepicker table tr td.range.focus {
|
||||
color: #000;
|
||||
background-color: #d2d2d3;
|
||||
border-color: #88888a;
|
||||
color: #212529;
|
||||
background-color: #d5d5d7;
|
||||
border-color: #909194;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range:hover {
|
||||
color: #000;
|
||||
background-color: #757575;
|
||||
border-color: #9f9fa2;
|
||||
color: #212529;
|
||||
background-color: #85878a;
|
||||
border-color: #a3a4a7;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range:active, .datepicker table tr td.range.active {
|
||||
color: #000;
|
||||
background-color: #d2d2d3;
|
||||
border-color: #9f9fa2;
|
||||
color: #212529;
|
||||
background-color: #d5d5d7;
|
||||
border-color: #a3a4a7;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range.focus:active, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
|
||||
color: #000;
|
||||
background-color: #c1c1c2;
|
||||
border-color: #88888a;
|
||||
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range:active.focus, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
|
||||
color: #212529;
|
||||
background-color: #c7c8c9;
|
||||
border-color: #909194;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range.focus[disabled], fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
|
||||
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.focus {
|
||||
background-color: #e9e9ea;
|
||||
border-color: #b5b5b8;
|
||||
}
|
||||
@@ -255,36 +264,39 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background-color: #ddebee;
|
||||
border-color: #99c3cc;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted:focus, .datepicker table tr td.range.highlighted.focus {
|
||||
color: #000;
|
||||
background-color: #c7d4d6;
|
||||
border-color: #739299;
|
||||
color: #212529;
|
||||
background-color: #cad7da;
|
||||
border-color: #7b9ca3;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted:hover {
|
||||
color: #000;
|
||||
background-color: #6f7677;
|
||||
border-color: #87acb4;
|
||||
color: #212529;
|
||||
background-color: #7f888c;
|
||||
border-color: #8bb0b8;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted:active, .datepicker table tr td.range.highlighted.active {
|
||||
color: #000;
|
||||
background-color: #c7d4d6;
|
||||
border-color: #87acb4;
|
||||
color: #212529;
|
||||
background-color: #cad7da;
|
||||
border-color: #8bb0b8;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted.focus:active, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
|
||||
color: #000;
|
||||
background-color: #b7c3c6;
|
||||
border-color: #739299;
|
||||
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted:active.focus, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
|
||||
color: #212529;
|
||||
background-color: #bdc9cd;
|
||||
border-color: #7b9ca3;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
|
||||
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
|
||||
background-color: #ddebee;
|
||||
border-color: #99c3cc;
|
||||
}
|
||||
@@ -299,36 +311,39 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background-color: #f4c775;
|
||||
border-color: #eca117;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today:focus, .datepicker table tr td.range.today.focus {
|
||||
color: #000;
|
||||
background-color: #dcb369;
|
||||
border-color: #b17811;
|
||||
color: #212529;
|
||||
background-color: #dfb76d;
|
||||
border-color: #ba821b;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today:hover {
|
||||
color: #000;
|
||||
background-color: #7a643b;
|
||||
border-color: #d08d14;
|
||||
color: #212529;
|
||||
background-color: #8b764f;
|
||||
border-color: #d49219;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today:active, .datepicker table tr td.range.today.active {
|
||||
color: #000;
|
||||
background-color: #dcb369;
|
||||
border-color: #d08d14;
|
||||
color: #212529;
|
||||
background-color: #dfb76d;
|
||||
border-color: #d49219;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today.focus:active, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
|
||||
color: #000;
|
||||
background-color: #cba561;
|
||||
border-color: #b17811;
|
||||
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today:active.focus, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
|
||||
color: #212529;
|
||||
background-color: #d0ab68;
|
||||
border-color: #ba821b;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today.focus[disabled], fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
|
||||
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.focus {
|
||||
background-color: #f4c775;
|
||||
border-color: #eca117;
|
||||
}
|
||||
@@ -363,13 +378,19 @@
|
||||
border-color: #7d7f82;
|
||||
}
|
||||
|
||||
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected.focus:active, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted.focus:active, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
|
||||
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected:active.focus, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted:active.focus, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
|
||||
color: #fff;
|
||||
background-color: #9d9fa0;
|
||||
border-color: #909295;
|
||||
}
|
||||
|
||||
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected.focus[disabled], fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
|
||||
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
|
||||
background-color: #898b8d;
|
||||
border-color: #6b6e71;
|
||||
}
|
||||
@@ -399,13 +420,19 @@
|
||||
border-color: #2087f5;
|
||||
}
|
||||
|
||||
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active.focus:active, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted.focus:active, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
|
||||
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active:active.focus, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted:active.focus, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
|
||||
color: #fff;
|
||||
background-color: #2b91ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
|
||||
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active.focus[disabled], fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
|
||||
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
}
|
||||
@@ -422,7 +449,7 @@
|
||||
}
|
||||
|
||||
.datepicker table tr td span:hover, .datepicker table tr td span.focused {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background: #e9e9ea;
|
||||
}
|
||||
|
||||
@@ -439,7 +466,7 @@
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover {
|
||||
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled:hover.focus {
|
||||
color: #fff;
|
||||
background-color: #1a88ff;
|
||||
border-color: #4199f7;
|
||||
@@ -451,19 +478,31 @@
|
||||
border-color: #2087f5;
|
||||
}
|
||||
|
||||
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled.active:hover {
|
||||
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active:hover.active, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled:hover.active {
|
||||
color: #fff;
|
||||
background-color: #1a88ff;
|
||||
border-color: #2087f5;
|
||||
}
|
||||
|
||||
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active.focus:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active.focus:hover:active, .datepicker table tr td span.active.active:hover:hover, .datepicker table tr td span.active.active:hover:focus, .datepicker table tr td span.active.active.focus:hover, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled.focus:active, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled.focus:hover:active, .datepicker table tr td span.active.disabled.active:hover:hover, .datepicker table tr td span.active.disabled.active:hover:focus, .datepicker table tr td span.active.disabled.active.focus:hover {
|
||||
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active:active.focus, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active:hover:active.focus, .datepicker table tr td span.active:hover.active:hover, .datepicker table tr td span.active:hover.active:focus, .datepicker table tr td span.active:hover.active.focus, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled:active.focus, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled:hover:active.focus, .datepicker table tr td span.active.disabled:hover.active:hover, .datepicker table tr td span.active.disabled:hover.active:focus, .datepicker table tr td span.active.disabled:hover.active.focus {
|
||||
color: #fff;
|
||||
background-color: #2b91ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
|
||||
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active.focus[disabled], fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active.disabled:hover:hover, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover, .datepicker table tr td span.active[disabled]:hover:hover, .datepicker table tr td span.active[disabled]:hover:focus, .datepicker table tr td span.active.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled.focus[disabled], fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled.disabled:hover:hover, .datepicker table tr td span.active.disabled.disabled:hover:focus, .datepicker table tr td span.active.disabled.disabled.focus:hover, .datepicker table tr td span.active.disabled[disabled]:hover:hover, .datepicker table tr td span.active.disabled[disabled]:hover:focus, .datepicker table tr td span.active.disabled.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus:hover {
|
||||
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover.disabled:hover, .datepicker table tr td span.active:hover.disabled:focus, .datepicker table tr td span.active:hover.disabled.focus, .datepicker table tr td span.active:hover[disabled]:hover, .datepicker table tr td span.active:hover[disabled]:focus, .datepicker table tr td span.active:hover[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover.disabled:hover, .datepicker table tr td span.active.disabled:hover.disabled:focus, .datepicker table tr td span.active.disabled:hover.disabled.focus, .datepicker table tr td span.active.disabled:hover[disabled]:hover, .datepicker table tr td span.active.disabled:hover[disabled]:focus, .datepicker table tr td span.active.disabled:hover[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
}
|
||||
@@ -487,7 +526,7 @@
|
||||
.datepicker .prev:hover,
|
||||
.datepicker .next:hover,
|
||||
.datepicker tfoot tr th:hover {
|
||||
color: #000;
|
||||
color: #212529;
|
||||
background: #e9e9ea;
|
||||
}
|
||||
|
||||
@@ -531,3 +570,4 @@
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
// Variables and mixins copied from Bootstrap 3.3.5
|
||||
|
||||
// These are BS3 variables that are used in datepicker3.scss. So, when compiling against
|
||||
// a BS3 bslib theme, these variables should already be defined. Here we set
|
||||
// a BS3 bootstraplib theme, these variables should already be defined. Here we set
|
||||
// *defaults* for these variables based on BS4 variables, so this scss can work for
|
||||
// both BS3 and BS4
|
||||
$gray: mix($body-bg, $body-color, 33.5%) !default;
|
||||
@@ -26,12 +26,27 @@ $dropdown-border: $dropdown-border-color !default;
|
||||
//$btn-link-disabled-color: $gray-light;
|
||||
//$dropdown-bg: #fff;
|
||||
|
||||
// Setup BS4 style color contrasting
|
||||
$yiq-contrasted-threshold: 150 !default;
|
||||
$yiq-text-dark: #212529 !default;
|
||||
$yiq-text-light: #fff !default;
|
||||
|
||||
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
|
||||
$r: red($color);
|
||||
$g: green($color);
|
||||
$b: blue($color);
|
||||
|
||||
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
@if ($yiq >= $yiq-contrasted-threshold) {
|
||||
@return $dark;
|
||||
} @else {
|
||||
@return $light;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-variant($background, $border) {
|
||||
$color: color-contrast($background);
|
||||
$color: color-yiq($background);
|
||||
|
||||
color: $color;
|
||||
background-color: $background;
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
// Both BS3 and BS4 define a border radius mixin, but just in case
|
||||
// we're trying to compile this without bootstrapSass
|
||||
@mixin border-radius-shim($radius) {
|
||||
@if mixin-exists("border-radius") {
|
||||
@include border-radius($radius);
|
||||
} @else {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
@include border-radius-shim($border-radius-base);
|
||||
border-radius: $border-radius-base;
|
||||
&-inline {
|
||||
width: 220px;
|
||||
}
|
||||
@@ -74,7 +64,7 @@
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@include border-radius-shim(4px);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +83,7 @@
|
||||
}
|
||||
&.day:hover,
|
||||
&.focused {
|
||||
color: color-contrast($gray-lighter);
|
||||
color: color-yiq($gray-lighter);
|
||||
background: $gray-lighter;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -189,10 +179,10 @@
|
||||
float: left;
|
||||
margin: 1%;
|
||||
cursor: pointer;
|
||||
@include border-radius-shim(4px);
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&.focused {
|
||||
color: color-contrast($gray-lighter);
|
||||
color: color-yiq($gray-lighter);
|
||||
background: $gray-lighter;
|
||||
}
|
||||
&.disabled,
|
||||
@@ -225,7 +215,7 @@
|
||||
tfoot tr th {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: color-contrast($gray-lighter);
|
||||
color: color-yiq($gray-lighter);
|
||||
background: $gray-lighter;
|
||||
}
|
||||
}
|
||||
@@ -253,10 +243,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
input:first-child {
|
||||
@include border-radius-shim(3px 0 0 3px);
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
input:last-child {
|
||||
@include border-radius-shim(0 3px 3px 0);
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.input-group-addon {
|
||||
width: auto;
|
||||
|
||||
@@ -261,3 +261,4 @@
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,25 @@
|
||||
|
||||
@import "_base";
|
||||
|
||||
// Both BS3 and BS4 define a border radius mixin, but just in case
|
||||
// we're trying to compile this without bootstrapSass
|
||||
@mixin border-radius-shim($radius) {
|
||||
@if mixin-exists("border-radius") {
|
||||
@include border-radius($radius);
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Setup BS4 style color contrasting
|
||||
$yiq-contrasted-threshold: 150 !default;
|
||||
$yiq-text-dark: #212529 !default;
|
||||
$yiq-text-light: #fff !default;
|
||||
|
||||
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
|
||||
$r: red($color);
|
||||
$g: green($color);
|
||||
$b: blue($color);
|
||||
|
||||
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
@if ($yiq >= $yiq-contrasted-threshold) {
|
||||
@return $dark;
|
||||
} @else {
|
||||
border-radius: $radius;
|
||||
@return $light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Re-define font-family on .irs to make it configurable
|
||||
@@ -58,7 +66,7 @@ $font-family: Arial, sans-serif !default;
|
||||
$minmax_line_height: 1.333 !default;
|
||||
|
||||
$fromto_bg_color: $accent !default;
|
||||
$fromto_color: color-contrast($fromto_bg_color) !default;
|
||||
$fromto_color: color-yiq($fromto_bg_color) !default;
|
||||
$fromto_font_size: 11px !default;
|
||||
$fromto_line_height: 1.333 !default;
|
||||
|
||||
@@ -79,7 +87,7 @@ $font-family: Arial, sans-serif !default;
|
||||
background: $line_bg;
|
||||
background-color: $line_bg_color;
|
||||
border: $line_border;
|
||||
@include border-radius-shim($line_height);
|
||||
border-radius: $line_height;
|
||||
}
|
||||
|
||||
.#{$name}-bar {
|
||||
@@ -90,7 +98,7 @@ $font-family: Arial, sans-serif !default;
|
||||
background: $bar_color;
|
||||
|
||||
&--single {
|
||||
@include border-radius-shim($line_height 0 0 $line_height);
|
||||
border-radius: $line_height 0 0 $line_height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +107,7 @@ $font-family: Arial, sans-serif !default;
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba($fg, 0.3);
|
||||
@include border-radius-shim(5px);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.lt-ie9 .#{$name}-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
@@ -114,7 +122,7 @@ $font-family: Arial, sans-serif !default;
|
||||
border: $handle_border;
|
||||
background-color: $handle_color;
|
||||
box-shadow: $handle_box_shadow;
|
||||
@include border-radius-shim($handle_width);
|
||||
border-radius: $handle_width;
|
||||
|
||||
&.state_hover,
|
||||
&:hover {
|
||||
@@ -129,7 +137,7 @@ $font-family: Arial, sans-serif !default;
|
||||
color: $minmax_text_color;
|
||||
text-shadow: none;
|
||||
background-color: $minmax_bg_color;
|
||||
@include border-radius-shim($custom_radius);
|
||||
border-radius: $custom_radius;
|
||||
font-size: $minmax_font_size;
|
||||
line-height: $minmax_line_height;
|
||||
}
|
||||
@@ -146,7 +154,7 @@ $font-family: Arial, sans-serif !default;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: $fromto_bg_color;
|
||||
@include border-radius-shim($custom_radius);
|
||||
border-radius: $custom_radius;
|
||||
font-size: $fromto_font_size;
|
||||
line-height: $fromto_line_height;
|
||||
}
|
||||
|
||||
@@ -57,11 +57,9 @@ $selectize-caret-margin: 0 2px 0 0 !default;
|
||||
$selectize-caret-margin-rtl: 0 4px 0 -2px !default;
|
||||
|
||||
@mixin selectize-border-radius($radii){
|
||||
@if mixin-exists("border-radius") {
|
||||
@include border-radius($radii)
|
||||
} @else {
|
||||
border-radius: $radii;
|
||||
}
|
||||
-webkit-border-radius: $radii;
|
||||
-moz-border-radius: $radii;
|
||||
border-radius: $radii;
|
||||
}
|
||||
@mixin selectize-unselectable(){
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -9,7 +9,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
(function () {
|
||||
var $ = jQuery;
|
||||
var exports = window.Shiny = window.Shiny || {};
|
||||
exports.version = "1.5.0.9005"; // Version number inserted by Grunt
|
||||
exports.version = "1.5.0.9004"; // Version number inserted by Grunt
|
||||
|
||||
var origPushState = window.history.pushState;
|
||||
|
||||
@@ -162,7 +162,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
function pixelRatio() {
|
||||
if (window.devicePixelRatio) {
|
||||
return Math.round(window.devicePixelRatio * 100) / 100;
|
||||
return window.devicePixelRatio;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
@@ -364,26 +364,18 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
} // Detect IE information
|
||||
|
||||
|
||||
var ua = window.navigator.userAgent;
|
||||
var isIE = /MSIE|Trident/.test(ua);
|
||||
var isIE = navigator.appName === 'Microsoft Internet Explorer';
|
||||
|
||||
function getIEVersion() {
|
||||
var msie = ua.indexOf('MSIE ');
|
||||
var rv = -1;
|
||||
|
||||
if (isIE && msie > 0) {
|
||||
// IE 10 or older => return version number
|
||||
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
|
||||
if (isIE) {
|
||||
var ua = navigator.userAgent;
|
||||
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
|
||||
if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1);
|
||||
}
|
||||
|
||||
var trident = ua.indexOf('Trident/');
|
||||
|
||||
if (trident > 0) {
|
||||
// IE 11 => return version number
|
||||
var rv = ua.indexOf('rv:');
|
||||
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
|
||||
}
|
||||
|
||||
return -1;
|
||||
return rv;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -3961,86 +3953,26 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
}
|
||||
|
||||
if (dep.stylesheet) {
|
||||
var links = $.map(asArray(dep.stylesheet), function (stylesheet) {
|
||||
return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
|
||||
var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) {
|
||||
var this_href = href + "/" + encodeURI(stylesheet); // If a styleSheet with this href is already active, then disable it
|
||||
|
||||
var re = new RegExp(this_href + "$");
|
||||
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var h = document.styleSheets[i].href;
|
||||
if (!h) continue;
|
||||
if (restyle && h.match(re)) document.styleSheets[i].disabled = true;
|
||||
} // Force client to re-request the stylesheet if we've already seen it
|
||||
// (without this unique param, the request might get cached)
|
||||
|
||||
|
||||
if (restyle) {
|
||||
this_href = this_href + "?restyle=" + new Date().getTime();
|
||||
}
|
||||
|
||||
return $("<link rel='stylesheet' type='text/css'>").attr("href", this_href);
|
||||
});
|
||||
|
||||
if (!restyle) {
|
||||
$head.append(links);
|
||||
} else {
|
||||
// This inline <style> based approach works for IE11
|
||||
var refreshStyle = function refreshStyle(href, oldSheet) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', href);
|
||||
|
||||
xhr.onload = function () {
|
||||
var id = "shiny_restyle_" + href.split("?restyle")[0].replace(/\W/g, '_');
|
||||
var oldStyle = $head.find("style#" + id);
|
||||
var newStyle = $("<style>").attr("id", id).html(xhr.responseText);
|
||||
$head.append(newStyle);
|
||||
setTimeout(function () {
|
||||
return oldStyle.remove();
|
||||
}, 500);
|
||||
setTimeout(function () {
|
||||
return removeSheet(oldSheet);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
var findSheet = function findSheet(href) {
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var sheet = document.styleSheets[i]; // The sheet's href is a full URL
|
||||
|
||||
if (typeof sheet.href === "string" && sheet.href.indexOf(href) > -1) {
|
||||
return sheet;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var removeSheet = function removeSheet(sheet) {
|
||||
if (!sheet) return;
|
||||
sheet.disabled = true;
|
||||
if (browser.isIE) sheet.cssText = "";
|
||||
$(sheet.ownerNode).remove();
|
||||
};
|
||||
|
||||
$.map(links, function (link) {
|
||||
// Find any document.styleSheets that match this link's href
|
||||
// so we can remove it after bringing in the new stylesheet
|
||||
var oldSheet = findSheet(link.attr("href")); // Add a timestamp to the href to prevent caching
|
||||
|
||||
var href = link.attr("href") + "?restyle=" + new Date().getTime(); // Use inline <style> approach for IE, otherwise use the more elegant
|
||||
// <link> -based approach
|
||||
|
||||
if (browser.isIE) {
|
||||
refreshStyle(href, oldSheet);
|
||||
} else {
|
||||
link.attr("href", href); // Once the new <link> is loaded, schedule the old <link> to be removed
|
||||
// on the next tick which is needed to avoid FOUC
|
||||
|
||||
link.attr("onload", function () {
|
||||
setTimeout(function () {
|
||||
return removeSheet(oldSheet);
|
||||
}, 500);
|
||||
});
|
||||
$head.append(link);
|
||||
}
|
||||
}); // Once the new styles are applied, CSS values that are accessible server-side
|
||||
// (e.g., getCurrentOutputInfo(), output visibility, etc) may become outdated.
|
||||
// At the time of writing, that means we need to do sendImageSize() &
|
||||
// sendOutputHiddenState() again, which can be done by re-binding.
|
||||
|
||||
/* global Shiny */
|
||||
|
||||
var bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
|
||||
setTimeout(function () {
|
||||
return bindDebouncer.normalCall();
|
||||
}, 100);
|
||||
}
|
||||
$head.append(stylesheets);
|
||||
}
|
||||
|
||||
if (dep.script && !restyle) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
inst/www/shared/shiny.min.css
vendored
2
inst/www/shared/shiny.min.css
vendored
File diff suppressed because one or more lines are too long
4
inst/www/shared/shiny.min.js
vendored
4
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
inst/www/shared/shiny_scss/bootstrap.scss
vendored
4
inst/www/shared/shiny_scss/bootstrap.scss
vendored
@@ -18,10 +18,8 @@ $shiny-table-na: mix($bg, $fg, 54%) !default;
|
||||
$shiny-error-validated-color: mix($bg, $fg, 50%) !default;
|
||||
// TODO: what should this be? mix of primary and bg?
|
||||
$shiny-progress-text-bg-color: #eef8ff !default;
|
||||
$shiny-input-panel-bg: mix($bg, $fg, 96%) !default;
|
||||
$shiny-input-panel-bg: mix($bg, $fg, 95%) !default;
|
||||
$shiny-input-panel-border: $border-width solid $border-color !default;
|
||||
$shiny-input-panel-border-radius: $border-radius !default;
|
||||
$shiny-text-output-border-radius: $border-radius !default;
|
||||
$notification-bg-color: mix($bg, $fg, 90%) !default;
|
||||
$notification-color: $fg !default;
|
||||
$notification-border: $border-width solid $border-color !default;
|
||||
|
||||
@@ -7,8 +7,6 @@ $shiny-progress-text-bg-color: #eef8ff !default; // TODO: what is this actu
|
||||
$shiny-input-width: 300px !default;
|
||||
$shiny-input-panel-bg: #f5f5f5 !default;
|
||||
$shiny-input-panel-border: 1px solid #e3e3e3 !default;
|
||||
$shiny-input-panel-border-radius: 2px !default;
|
||||
$shiny-text-output-border-radius: null !default;
|
||||
$notification-bg-color: #e8e8e8 !default;
|
||||
$notification-color: #333 !default;
|
||||
$notification-border: 1px solid #ccc !default;
|
||||
@@ -44,16 +42,6 @@ $shiny-file-over-shadow: inset 0 1px 1px rgba(black, .075), 0 0 8px r
|
||||
}
|
||||
}
|
||||
|
||||
// Both BS3 and BS4 define a border radius mixin, but just in case
|
||||
// we're trying to compile this without bootstrapSass
|
||||
@mixin border-radius-shim($radius) {
|
||||
@if mixin-exists("border-radius") {
|
||||
@include border-radius($radius);
|
||||
} @else {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// RULES
|
||||
|
||||
@@ -76,7 +64,6 @@ pre.shiny-text-output.noplaceholder:empty {
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
@include border-radius-shim($shiny-text-output-border-radius);
|
||||
}
|
||||
|
||||
.shiny-image-output, .shiny-plot-output {
|
||||
@@ -329,7 +316,7 @@ pre.shiny-text-output {
|
||||
margin-bottom: 6px;
|
||||
background-color: $shiny-input-panel-bg;
|
||||
border: $shiny-input-panel-border;
|
||||
@include border-radius-shim($shiny-input-panel-border-radius);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
@@ -377,7 +364,7 @@ pre.shiny-text-output {
|
||||
background-color: $notification-bg-color;
|
||||
color: $notification-color;
|
||||
border: $notification-border;
|
||||
@include border-radius-shim($notification-border-radius);
|
||||
border-radius: $notification-border-radius;
|
||||
opacity: 0.85;
|
||||
padding: $notification-padding;
|
||||
margin: 2px;
|
||||
|
||||
@@ -50,9 +50,9 @@ session$setInputs(x=1, y=2)
|
||||
|
||||
\item{\code{token}}{On a real \code{ShinySession}, used to identify this instance in URLs.}
|
||||
|
||||
\item{\code{cache}}{The session cache object.}
|
||||
\item{\code{cache}}{The session cache MemoryCache.}
|
||||
|
||||
\item{\code{appcache}}{The app cache object.}
|
||||
\item{\code{appcache}}{The app cache MemoryCache.}
|
||||
|
||||
\item{\code{restoreContext}}{Part of bookmarking support in a real
|
||||
\code{ShinySession} but always \code{NULL} for a \code{MockShinySession}.}
|
||||
|
||||
423
man/bindCache.Rd
423
man/bindCache.Rd
@@ -1,423 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/bind-cache.R
|
||||
\name{bindCache}
|
||||
\alias{bindCache}
|
||||
\title{Add caching with reactivity to an object}
|
||||
\usage{
|
||||
bindCache(x, ..., cache = "app")
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{The object to add caching to.}
|
||||
|
||||
\item{...}{One or more expressions to use in the caching key.}
|
||||
|
||||
\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[cachem:cache_disk]{cachem::cache_disk()}}. See the Cache Scoping section for more information.}
|
||||
}
|
||||
\description{
|
||||
\code{bindCache()} adds caching \code{\link[=reactive]{reactive()}} expressions and \verb{render*} functions
|
||||
(like \code{\link[=renderText]{renderText()}}, \code{\link[=renderTable]{renderTable()}}, ...).
|
||||
|
||||
Ordinary \code{\link[=reactive]{reactive()}} expressions automatically cache their \emph{most recent}
|
||||
value, which helps to avoid redundant computation in downstream reactives.
|
||||
\code{bindCache()} will cache all previous values (as long as they fit in the
|
||||
cache) and they can be shared across user sessions. This allows
|
||||
\code{bindCache()} to dramatically improve performance when used correctly.
|
||||
}
|
||||
\details{
|
||||
\code{bindCache()} requires one or more expressions that are used to generate a
|
||||
\strong{cache key}, which is used to determine if a computation has occurred
|
||||
before and hence can be retrieved from the cache. If you're familiar with the
|
||||
concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
|
||||
can think of the cache key as the input(s) to a pure function. As such, one
|
||||
should take care to make sure the use of \code{bindCache()} is \emph{pure} in the same
|
||||
sense, namely:
|
||||
\enumerate{
|
||||
\item For a given key, the return value is always the same.
|
||||
\item Evaluation has no side-effects.
|
||||
}
|
||||
|
||||
In the example here, the \code{bindCache()} key consists of \code{input$x} and
|
||||
\code{input$y} combined, and the value is \code{input$x * input$y}. In this simple
|
||||
example, for any given key, there is only one possible returned value.\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ input$x * input$y \}) \%>\%
|
||||
bindCache(input$x, input$y)
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
The largest performance improvements occur when the cache key is fast to
|
||||
compute and the reactive expression is slow to compute. To see if the value
|
||||
should be computed, a cached reactive evaluates the key, and then serializes
|
||||
and hashes the result. If the resulting hashed key is in the cache, then the
|
||||
cached reactive simply retrieves the previously calculated value and returns
|
||||
it; if not, then the value is computed and the result is stored in the cache
|
||||
before being returned.
|
||||
|
||||
To compute the cache key, \code{bindCache()} hashes the contents of \code{...}, so it's
|
||||
best to avoid including large objects in a cache key since that can result in
|
||||
slow hashing. It's also best to avoid reference objects like environments and
|
||||
R6 objects, since the serialization of these objects may not capture relevant
|
||||
changes.
|
||||
|
||||
If you want to use a large object as part of a cache key, it may make sense
|
||||
to do some sort of reduction on the data that still captures information
|
||||
about whether a value can be retrieved from the cache. For example, if you
|
||||
have a large data set with timestamps, it might make sense to extract the
|
||||
most recent timestamp and return that. Then, instead of hashing the entire
|
||||
data object, the cached reactive only needs to hash the timestamp.\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ compute(bigdata()) \} \%>\%
|
||||
bindCache(\{ extract_most_recent_time(bigdata()) \})
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
For computations that are vert slow, it often makes sense to pair
|
||||
\code{\link[=bindCache]{bindCache()}} with \code{\link[=bindEvent]{bindEvent()}} so that no computation is performed until
|
||||
the user explicitly requests it (for more, see the Details section of
|
||||
\code{\link[=bindEvent]{bindEvent()}}).
|
||||
}
|
||||
\section{Cache keys and reactivity}{
|
||||
|
||||
|
||||
Because the \strong{value} expression (from the original \code{\link[=reactive]{reactive()}}) is
|
||||
cached, it is not necessarily re-executed when someone retrieves a value,
|
||||
and therefore it can't be used to decide what objects to take reactive
|
||||
dependencies on. Instead, the \strong{key} is used to figure out which objects
|
||||
to take reactive dependencies on. In short, the key expression is reactive,
|
||||
and value expression is no longer reactive.
|
||||
|
||||
Here's an example of what not to do: if the key is \code{input$x} and the value
|
||||
expression is from \code{reactive({input$x + input$y})}, then the resulting
|
||||
cached reactive will only take a reactive dependency on \code{input$x} -- it
|
||||
won't recompute \code{{input$x + input$y}} when just \code{input$y} changes.
|
||||
Moreover, the cache won't use \code{input$y} as part of the key, and so it could
|
||||
return incorrect values in the future when it retrieves values from the
|
||||
cache. (See the examples below for an example of this.)
|
||||
|
||||
A better cache key would be something like \verb{input$x, input$y}. This does
|
||||
two things: it ensures that a reactive dependency is taken on both
|
||||
\code{input$x} and \code{input$y}, and it also makes sure that both values are
|
||||
represented in the cache key.
|
||||
|
||||
In general, \code{key} should use the same reactive inputs as \code{value}, but the
|
||||
computation should be simpler. If there are other (non-reactive) values
|
||||
that are consumed, such as external data sources, they should be used in
|
||||
the \code{key} as well. Note that if the \code{key} is large, it can make sense to do
|
||||
some sort of reduction on it so that the serialization and hashing of the
|
||||
cache key is not too expensive.
|
||||
|
||||
Remember that the key is \emph{reactive}, so it is not re-executed every single
|
||||
time that someone accesses the cached reactive. It is only re-executed if
|
||||
it has been invalidated by one of the reactives it depends on. For
|
||||
example, suppose we have this cached reactive:\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ input$x * input$y \}) \%>\%
|
||||
bindCache(input$x, input$y)
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
In this case, the key expression is essentially \code{reactive(list(input$x, input$y))} (there's a bit more to it, but that's a good enough
|
||||
approximation). The first time \code{r()} is called, it executes the key, then
|
||||
fails to find it in the cache, so it executes the value expression, \code{{ input$x + input$y }}. If \code{r()} is called again, then it does not need to
|
||||
re-execute the key expression, because it has not been invalidated via a
|
||||
change to \code{input$x} or \code{input$y}; it simply returns the previous value.
|
||||
However, if \code{input$x} or \code{input$y} changes, then the reactive expression will
|
||||
be invalidated, and the next time that someone calls \code{r()}, the key
|
||||
expression will need to be re-executed.
|
||||
|
||||
Note that if the cached reactive is passed to \code{\link[=bindEvent]{bindEvent()}}, then the key
|
||||
expression will no longer be reactive; instead, the event expression will be
|
||||
reactive.
|
||||
}
|
||||
|
||||
\section{Cache scope}{
|
||||
|
||||
|
||||
By default, when \code{bindCache()} is used, it is scoped to the running
|
||||
application. That means that it shares a cache with all user sessions
|
||||
connected to the application (within the R process). This is done with the
|
||||
\code{cache} parameter's default value, \code{"app"}.
|
||||
|
||||
With an app-level cache scope, one user can benefit from the work done for
|
||||
another user's session. In most cases, this is the best way to get
|
||||
performance improvements from caching. However, in some cases, this could
|
||||
leak information between sessions. For example, if the cache key does not
|
||||
fully encompass the inputs used by the value, then data could leak between
|
||||
the sessions. Or if a user sees that a cached reactive returns its value
|
||||
very quickly, they may be able to infer that someone else has already used
|
||||
it with the same values.
|
||||
|
||||
It is also possible to scope the cache to the session, with
|
||||
\code{cache="session"}. This removes the risk of information leaking between
|
||||
sessions, but then one session cannot benefit from computations performed in
|
||||
another session.
|
||||
|
||||
It is possible to pass in caching objects directly to
|
||||
\code{bindCache()}. This can be useful if, for example, you want to use a
|
||||
particular type of cache with specific cached reactives, or if you want to
|
||||
use a \code{\link[cachem:cache_disk]{cachem::cache_disk()}} that is shared across multiple processes and
|
||||
persists beyond the current R session.
|
||||
|
||||
To use different settings for an application-scoped cache, you can call
|
||||
\code{\link[=shinyOptions]{shinyOptions()}} at the top of your app.R, server.R, or
|
||||
global.R. For example, this will create a cache with 500 MB of space
|
||||
instead of the default 200 MB:\preformatted{shinyOptions(cache = cachem::cache_mem(size = 500e6))
|
||||
}
|
||||
|
||||
To use different settings for a session-scoped cache, you can set
|
||||
\code{self$cache} at the top of your server function. By default, it will create
|
||||
a 200 MB memory cache for each session, but you can replace it with
|
||||
something different. To use the session-scoped cache, you must also call
|
||||
\code{bindCache()} with \code{cache="session"}. This will create a 100 MB cache for
|
||||
the session:\preformatted{function(input, output, session) \{
|
||||
session$cache <- cachem::cache_mem(size = 100e6)
|
||||
...
|
||||
\}
|
||||
}
|
||||
|
||||
If you want to use a cache that is shared across multiple R processes, you
|
||||
can use a \code{\link[cachem:cache_disk]{cachem::cache_disk()}}. You can create a application-level shared
|
||||
cache by putting this at the top of your app.R, server.R, or global.R:\preformatted{shinyOptions(cache = cachem::cache_disk(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 = cachem::cache_disk("./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 item, or selected items. To do that,
|
||||
create a \code{\link[cachem:cache_mem]{cachem::cache_mem()}} or \code{\link[cachem:cache_disk]{cachem::cache_disk()}}, and pass it
|
||||
as the \code{cache} argument of \code{bindCache()}.
|
||||
}
|
||||
|
||||
\section{Computing cache keys}{
|
||||
|
||||
|
||||
The actual cache key that is used internally takes value from evaluating
|
||||
the key expression(s) (from the \code{...} arguments) and combines it with the
|
||||
(unevaluated) value expression.
|
||||
|
||||
This means that if there are two cached reactives which have the same
|
||||
result from evaluating the key, but different value expressions, then they
|
||||
will not need to worry about collisions.
|
||||
|
||||
However, if two cached reactives have identical key and value expressions
|
||||
expressions, they will share the cached values. This is useful when using
|
||||
\code{cache="app"}: there may be multiple user sessions which create separate
|
||||
cached reactive objects (because they are created from the same code in the
|
||||
server function, but the server function is executed once for each user
|
||||
session), and those cached reactive objects across sessions can share
|
||||
values in the cache.
|
||||
}
|
||||
|
||||
\section{Async with cached reactives}{
|
||||
|
||||
|
||||
With a cached reactive expression, the key and/or value expression can be
|
||||
\emph{asynchronous}. In other words, they can be promises --- not regular R
|
||||
promises, but rather objects provided by the
|
||||
\href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
|
||||
are similar to promises in JavaScript. (See \code{\link[promises:promise]{promises::promise()}} for more
|
||||
information.) You can also use \code{\link[future:future]{future::future()}} objects to run code in a
|
||||
separate process or even on a remote machine.
|
||||
|
||||
If the value returns a promise, then anything that consumes the cached
|
||||
reactive must expect it to return a promise.
|
||||
|
||||
Similarly, if the key is a promise (in other words, if it is asynchronous),
|
||||
then the entire cached reactive must be asynchronous, since the key must be
|
||||
computed asynchronously before it knows whether to compute the value or the
|
||||
value is retrieved from the cache. Anything that consumes the cached
|
||||
reactive must therefore expect it to return a promise.
|
||||
}
|
||||
|
||||
\section{Developing render functions for caching}{
|
||||
|
||||
|
||||
If you've implemented your own \verb{render*()} function, you may need to
|
||||
provide a \code{cacheHint} to \code{\link[=createRenderFunction]{createRenderFunction()}} (or
|
||||
\code{\link[htmlwidgets:htmlwidgets-shiny]{htmlwidgets::shinyRenderWidget()}}, if you've authored an htmlwidget) in
|
||||
order for \code{bindCache()} to correctly compute a cache key.
|
||||
|
||||
The potential problem is a cache collision. Consider the following:\preformatted{output$x1 <- renderText(\{ input$x \}) \%>\% bindCache(input$x)
|
||||
output$x2 <- renderText(\{ input$x * 2 \}) \%>\% bindCache(input$x)
|
||||
}
|
||||
|
||||
Both \code{output$x1} and \code{output$x2} use \code{input$x} as part of their cache key,
|
||||
but if it were the only thing used in the cache key, then the two outputs
|
||||
would have a cache collision, and they would have the same output. To avoid
|
||||
this, a \emph{cache hint} is automatically added when \code{\link[=renderText]{renderText()}} calls
|
||||
\code{\link[=createRenderFunction]{createRenderFunction()}}. The cache hint is used as part of the actual
|
||||
cache key, in addition to the one passed to \code{bindCache()} by the user. The
|
||||
cache hint can be viewed by calling the internal Shiny function
|
||||
\code{extractCacheHint()}:\if{html}{\out{<div class="NA">}}\preformatted{r <- renderText(\{ input$x \})
|
||||
shiny:::extractCacheHint(r)
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
This returns a nested list containing an item, \verb{$origUserFunc$body}, which
|
||||
in this case is the expression which was passed to \code{renderText()}:
|
||||
\code{{ input$x }}. This (quoted) expression is mixed into the actual cache
|
||||
key, and it is how \code{output$x1} does not have collisions with \code{output$x2}.
|
||||
|
||||
For most developers of render functions, nothing extra needs to be done;
|
||||
the automatic inference of the cache hint is sufficient. Again, you can
|
||||
check it by calling \code{shiny:::extractCacheHint()}, and by testing the
|
||||
render function for cache collisions in a real application.
|
||||
|
||||
In some cases, however, the automatic cache hint inference is not
|
||||
sufficient, and it is necessary to provide a cache hint. This is true
|
||||
for \code{renderPrint()}. Unlike \code{renderText()}, it wraps the user-provided
|
||||
expression in another function, before passing it to \code{\link[=markRenderFunction]{markRenderFunction()}}
|
||||
(instead of \code{\link[=createRenderFunction]{createRenderFunction()}}). Because the user code is wrapped in
|
||||
another function, markRenderFunction() is not able to automatically extract
|
||||
the user-provided code and use it in the cache key. Instead, \code{renderPrint}
|
||||
calls \code{markRenderFunction()}, it explicitly passes along a \code{cacheHint},
|
||||
which includes a label and the original user expression.
|
||||
|
||||
In general, if you need to provide a \code{cacheHint}, it is best practice to
|
||||
provide a \code{label} id, the user's \code{expr}, as well as any other arguments
|
||||
that may influence the final value.
|
||||
|
||||
For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
|
||||
again, you can inspect the cache hint with \code{shiny:::extractCacheHint()} and
|
||||
also test it in an application. If you do need to explicitly provide a
|
||||
cache hint, pass it to \code{shinyRenderWidget}. For example:\preformatted{renderMyWidget <- function(expr) \{
|
||||
expr <- substitute(expr)
|
||||
|
||||
htmlwidgets::shinyRenderWidget(expr,
|
||||
myWidgetOutput,
|
||||
quoted = TRUE,
|
||||
env = parent.frame(),
|
||||
cacheHint = list(label = "myWidget", userExpr = expr)
|
||||
)
|
||||
\}
|
||||
}
|
||||
}
|
||||
|
||||
\section{Uncacheable objects}{
|
||||
|
||||
|
||||
Some render functions cannot be cached, typically because they have side
|
||||
effects or modify some external state, and they must re-execute each time
|
||||
in order to work properly.
|
||||
|
||||
For developers of such code, they should call \code{\link[=createRenderFunction]{createRenderFunction()}} or
|
||||
\code{\link[=markRenderFunction]{markRenderFunction()}} with \code{cacheHint = FALSE}.
|
||||
}
|
||||
|
||||
\section{Caching with \code{renderPlot()}}{
|
||||
|
||||
|
||||
When \code{bindCache()} is used with \code{renderPlot()}, the \code{height} and \code{width}
|
||||
passed to the original \code{renderPlot()} are ignored. They are superseded by
|
||||
\code{sizePolicy} argument passed to `bindCache. The default is:\preformatted{sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
}
|
||||
|
||||
\code{sizePolicy} must be a function that takes a two-element numeric vector as
|
||||
input, representing the width and height of the \verb{<img>} element in the
|
||||
browser window, and it must return a two-element numeric vector, representing
|
||||
the pixel dimensions of the plot to generate. 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]{sizeGrowthRatio()}} for more information on the default sizing policy.
|
||||
}
|
||||
|
||||
\examples{
|
||||
\dontrun{
|
||||
rc <- bindCache(
|
||||
x = reactive({
|
||||
Sys.sleep(2) # Pretend this is expensive
|
||||
input$x * 100
|
||||
}),
|
||||
input$x
|
||||
)
|
||||
|
||||
# Can make it prettier with the \%>\% operator
|
||||
library(magrittr)
|
||||
|
||||
rc <- reactive({
|
||||
Sys.sleep(2)
|
||||
input$x * 100
|
||||
}) \%>\%
|
||||
bindCache(input$x)
|
||||
|
||||
}
|
||||
|
||||
## Only run app examples in interactive R sessions
|
||||
if (interactive()) {
|
||||
|
||||
# Basic example
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
sliderInput("x", "x", 1, 10, 5),
|
||||
sliderInput("y", "y", 1, 10, 5),
|
||||
div("x * y: "),
|
||||
verbatimTextOutput("txt")
|
||||
),
|
||||
server = function(input, output) {
|
||||
r <- reactive({
|
||||
# The value expression is an _expensive_ computation
|
||||
message("Doing expensive computation...")
|
||||
Sys.sleep(2)
|
||||
input$x * input$y
|
||||
}) \%>\%
|
||||
bindCache(input$x, input$y)
|
||||
|
||||
output$txt <- renderText(r())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Caching renderText
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
sliderInput("x", "x", 1, 10, 5),
|
||||
sliderInput("y", "y", 1, 10, 5),
|
||||
div("x * y: "),
|
||||
verbatimTextOutput("txt")
|
||||
),
|
||||
server = function(input, output) {
|
||||
output$txt <- renderText({
|
||||
message("Doing expensive computation...")
|
||||
Sys.sleep(2)
|
||||
input$x * input$y
|
||||
}) \%>\%
|
||||
bindCache(input$x, input$y)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Demo of using events and caching with an actionButton
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
sliderInput("x", "x", 1, 10, 5),
|
||||
sliderInput("y", "y", 1, 10, 5),
|
||||
actionButton("go", "Go"),
|
||||
div("x * y: "),
|
||||
verbatimTextOutput("txt")
|
||||
),
|
||||
server = function(input, output) {
|
||||
r <- reactive({
|
||||
message("Doing expensive computation...")
|
||||
Sys.sleep(2)
|
||||
input$x * input$y
|
||||
}) \%>\%
|
||||
bindCache(input$x, input$y) \%>\%
|
||||
bindEvent(input$go)
|
||||
# The cached, eventified reactive takes a reactive dependency on
|
||||
# input$go, but doesn't use it for the cache key. It uses input$x and
|
||||
# input$y for the cache key, but doesn't take a reactive depdency on
|
||||
# them, because the reactive dependency is superseded by addEvent().
|
||||
|
||||
output$txt <- renderText(r())
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=bindEvent]{bindEvent()}}, \code{\link[=renderCachedPlot]{renderCachedPlot()}} for caching plots.
|
||||
}
|
||||
179
man/bindEvent.Rd
179
man/bindEvent.Rd
@@ -1,179 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/bind-event.R
|
||||
\name{bindEvent}
|
||||
\alias{bindEvent}
|
||||
\title{Make an object respond only to specified reactive events}
|
||||
\usage{
|
||||
bindEvent(
|
||||
x,
|
||||
...,
|
||||
ignoreNULL = TRUE,
|
||||
ignoreInit = FALSE,
|
||||
once = FALSE,
|
||||
label = NULL
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{An object to wrap so that is triggered only when a the specified
|
||||
event occurs.}
|
||||
|
||||
\item{...}{One or more expressions that represents the event; this can be a
|
||||
simple reactive value like \code{input$click}, a call to a reactive expression
|
||||
like \code{dataset()}, or even a complex expression inside curly braces. If
|
||||
there are multiple expressions in the \code{...}, then it will take a dependency
|
||||
on all of them.}
|
||||
|
||||
\item{ignoreNULL}{Whether the action should be triggered (or value
|
||||
calculated) when the input is \code{NULL}. See Details.}
|
||||
|
||||
\item{ignoreInit}{If \code{TRUE}, then, when the eventified object is first
|
||||
created/initialized, don't trigger the action or (compute the value). The
|
||||
default is \code{FALSE}. See Details.}
|
||||
|
||||
\item{once}{Used only for observers. Whether this \code{observer} should be
|
||||
immediately destroyed after the first time that the code in the observer is
|
||||
run. This pattern is useful when you want to subscribe to a event that
|
||||
should only happen once.}
|
||||
|
||||
\item{label}{A label for the observer or reactive, useful for debugging.}
|
||||
}
|
||||
\description{
|
||||
Modify an object to respond to "event-like" reactive inputs, values, and
|
||||
expressions. \code{bindEvent()} can be used with reactive expressions, render
|
||||
functions, and observers. The resulting object takes a reactive dependency on
|
||||
the \code{...} arguments, and not on the original object's code. This can, for
|
||||
example, be used to make an observer execute only when a button is pressed.
|
||||
}
|
||||
\section{Details}{
|
||||
|
||||
|
||||
Shiny's reactive programming framework is primarily designed for calculated
|
||||
values (reactive expressions) and side-effect-causing actions (observers)
|
||||
that respond to \emph{any} of their inputs changing. That's often what is
|
||||
desired in Shiny apps, but not always: sometimes you want to wait for a
|
||||
specific action to be taken from the user, like clicking an
|
||||
\code{\link[=actionButton]{actionButton()}}, before calculating an expression or taking an action. A
|
||||
reactive value or expression that is used to trigger other calculations in
|
||||
this way is called an \emph{event}.
|
||||
|
||||
These situations demand a more imperative, "event handling" style of
|
||||
programming that is possible--but not particularly intuitive--using the
|
||||
reactive programming primitives \code{\link[=observe]{observe()}} and \code{\link[=isolate]{isolate()}}. \code{bindEvent()}
|
||||
provides a straightforward API for event handling that wraps \code{observe} and
|
||||
\code{isolate}.
|
||||
|
||||
The \code{...} arguments are captured as expressions and combined into an
|
||||
\strong{event expression}. When this event expression is invalidated (when its
|
||||
upstream reactive inputs change), that is an \strong{event}, and it will cause
|
||||
the original object's code to execute.
|
||||
|
||||
Use \code{bindEvent()} with \code{observe()} whenever you want to \emph{perform an action}
|
||||
in response to an event. (Note that "recalculate a value" does not
|
||||
generally count as performing an action -- use \code{\link[=reactive]{reactive()}} for that.) The
|
||||
first argument is observer whose code should be executed whenever the event
|
||||
occurs.
|
||||
|
||||
Use \code{bindEvent()} with \code{reactive()} to create a \emph{calculated value} that only
|
||||
updates in response to an event. This is just like a normal \link[=reactive]{reactive expression} except it ignores all the usual invalidations that
|
||||
come from its reactive dependencies; it only invalidates in response to the
|
||||
given event.
|
||||
|
||||
\code{bindEvent()} is often used with \code{\link[=bindCache]{bindCache()}}.
|
||||
}
|
||||
|
||||
\section{ignoreNULL and ignoreInit}{
|
||||
|
||||
|
||||
\code{bindEvent()} takes an \code{ignoreNULL} parameter that affects behavior when
|
||||
the event expression evaluates to \code{NULL} (or in the special case of an
|
||||
\code{\link[=actionButton]{actionButton()}}, \code{0}). In these cases, if \code{ignoreNULL} is \code{TRUE}, then it
|
||||
will raise a silent \link[=validate]{validation} error. This is useful behavior
|
||||
if you don't want to do the action or calculation when your app first
|
||||
starts, but wait for the user to initiate the action first (like a "Submit"
|
||||
button); 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).
|
||||
|
||||
\code{bindEvent()} also takes an \code{ignoreInit} argument. By default, reactive
|
||||
expressions and observers will run on the first reactive flush after they
|
||||
are created (except if, at that moment, the event expression 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 observer to respond to a dynamically
|
||||
created button, then \code{ignoreInit = TRUE} will guarantee that the action
|
||||
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 a reactive that responds to a dynamically created button used to
|
||||
refresh some data (which is then returned by that \code{reactive}), then you
|
||||
should use \code{reactive(...) \%>\% bindEvent(..., 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).
|
||||
|
||||
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
|
||||
these:
|
||||
|
||||
\describe{
|
||||
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
|
||||
This is the default. This combination means that reactive/observer code
|
||||
will run every time that event expression is not
|
||||
\code{NULL}. If, at the time of creation, the event expression happens
|
||||
to \emph{not} be \code{NULL}, then the code runs.
|
||||
}
|
||||
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
|
||||
This combination means that reactive/observer code will
|
||||
run every time no matter what.
|
||||
}
|
||||
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
|
||||
This combination means that reactive/observer code will
|
||||
\emph{not} run at the time of creation (because \code{ignoreInit = TRUE}),
|
||||
but it will run every other time.
|
||||
}
|
||||
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
|
||||
This combination means that reactive/observer code will
|
||||
\emph{not} at the time of creation (because \code{ignoreInit = TRUE}).
|
||||
After that, the reactive/observer code will run every time that
|
||||
the event expression is not \code{NULL}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\section{Types of objects}{
|
||||
|
||||
|
||||
\code{bindEvent()} can be used with reactive expressions, observers, and shiny
|
||||
render functions.
|
||||
|
||||
When \code{bindEvent()} is used with \code{reactive()}, it creates a new reactive
|
||||
expression object.
|
||||
|
||||
When \code{bindEvent()} is used with \code{observe()}, it alters the observer in
|
||||
place. It can only be used with observers which have not yet executed.
|
||||
}
|
||||
|
||||
\section{Combining events and caching}{
|
||||
|
||||
|
||||
In many cases, it makes sense to use \code{bindEvent()} along with
|
||||
\code{bindCache()}, because they each can reduce the amount of work done on the
|
||||
server. For example, you could have \link{sliderInput}s \code{x} and \code{y} and a
|
||||
\code{reactive()} that performs a time-consuming operation with those values.
|
||||
Using \code{bindCache()} can speed things up, especially if there are multiple
|
||||
users. But it might make sense to also not do the computation until the
|
||||
user sets both \code{x} and \code{y}, and then clicks on an \link{actionButton} named
|
||||
\code{go}.
|
||||
|
||||
To use both caching and events, the object should first be passed to
|
||||
\code{bindCache()}, then \code{bindEvent()}. For example:\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{
|
||||
Sys.sleep(2) # Pretend this is an expensive computation
|
||||
input$x * input$y
|
||||
\}) \%>\%
|
||||
bindCache(input$x, input$y) \%>\%
|
||||
bindEvent(input$go)
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
Anything that consumes \code{r()} will take a reactive dependency on the event
|
||||
expression given to \code{bindEvent()}, and not the cache key expression given to
|
||||
\code{bindCache()}. In this case, it is just \code{input$go}.
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ bootstrapLib(theme = NULL)
|
||||
\item{theme}{One of the following:
|
||||
\itemize{
|
||||
\item \code{NULL} (the default), which implies a "stock" build of Bootstrap 3.
|
||||
\item A \code{\link[bslib:bs_theme]{bslib::bs_theme()}} object. This can be used to replace a stock
|
||||
\item A \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}} object. This can be used to replace a stock
|
||||
build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
|
||||
\item A character string pointing to an alternative Bootstrap stylesheet
|
||||
(normally a css file within the www directory, e.g. \code{www/bootstrap.css}).
|
||||
|
||||
@@ -20,7 +20,7 @@ Bootstrap 3.}
|
||||
\item{theme}{One of the following:
|
||||
\itemize{
|
||||
\item \code{NULL} (the default), which implies a "stock" build of Bootstrap 3.
|
||||
\item A \code{\link[bslib:bs_theme]{bslib::bs_theme()}} object. This can be used to replace a stock
|
||||
\item A \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}} object. This can be used to replace a stock
|
||||
build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
|
||||
\item A character string pointing to an alternative Bootstrap stylesheet
|
||||
(normally a css file within the www directory, e.g. \code{www/bootstrap.css}).
|
||||
|
||||
@@ -8,10 +8,7 @@ createRenderFunction(
|
||||
func,
|
||||
transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL,
|
||||
outputArgs = NULL,
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
outputArgs = NULL
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
@@ -29,65 +26,17 @@ JSON-encoded and sent to the browser.}
|
||||
this render function. This can be used in R Markdown documents to create
|
||||
complete output widgets out of just the render function.}
|
||||
|
||||
\item{outputArgs}{A list of arguments to pass to the \code{uiFunc}. Render
|
||||
functions should include \code{outputArgs = list()} in their own parameter list,
|
||||
and pass through the value to \code{markRenderFunction}, to allow app authors to
|
||||
customize outputs. (Currently, this is only supported for dynamically
|
||||
generated UIs, such as those created by Shiny code snippets embedded in R
|
||||
Markdown documents).}
|
||||
|
||||
\item{cacheHint}{One of \code{"auto"}, \code{FALSE}, or some other information to
|
||||
identify this instance for caching using \code{\link[=bindCache]{bindCache()}}. If \code{"auto"}, it
|
||||
will try to automatically infer caching information. If \code{FALSE}, do not
|
||||
allow caching for the object. Some render functions (such as \link{renderPlot})
|
||||
contain internal state that makes them unsuitable for caching.}
|
||||
|
||||
\item{cacheWriteHook}{Used if the render function is passed to \code{bindCache()}.
|
||||
This is an optional callback function to invoke before saving the value
|
||||
from the render function to the cache. This function must accept one
|
||||
argument, the value returned from \code{renderFunc}, and should return the value
|
||||
to store in the cache.}
|
||||
|
||||
\item{cacheReadHook}{Used if the render function is passed to \code{bindCache()}.
|
||||
This is an optional callback function to invoke after reading a value from
|
||||
the cache (if there is a cache hit). The function will be passed one
|
||||
argument, the value retrieved from the cache. This can be useful when some
|
||||
side effect needs to occur for a render function to behave correctly. For
|
||||
example, some render functions call \code{\link[=createWebDependency]{createWebDependency()}} so that Shiny
|
||||
is able to serve JS and CSS resources.}
|
||||
\item{outputArgs}{A list of arguments to pass to the \code{outputFunc}.
|
||||
Render functions should include \code{outputArgs = list()} in their own
|
||||
parameter list, and pass through the value as this argument, to allow app
|
||||
authors to customize outputs. (Currently, this is only supported for
|
||||
dynamically generated UIs, such as those created by Shiny code snippets
|
||||
embedded in R Markdown documents).}
|
||||
}
|
||||
\value{
|
||||
An annotated render function, ready to be assigned to an
|
||||
\code{output} slot.
|
||||
}
|
||||
\description{
|
||||
This function is a wrapper for \code{\link[=markRenderFunction]{markRenderFunction()}} which provides support
|
||||
for async computation via promises.
|
||||
}
|
||||
\examples{
|
||||
# A very simple render function
|
||||
renderTriple <- function(x) {
|
||||
x <- substitute(x)
|
||||
if (!rlang::is_quosure(x)) {
|
||||
x <- rlang::new_quosure(x, env = parent.frame())
|
||||
}
|
||||
func <- quoToFunction(x, "renderTriple")
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
transform = function(value, session, name, ...) {
|
||||
paste(rep(value, 3), collapse=", ")
|
||||
},
|
||||
outputFunc = textOutput
|
||||
)
|
||||
}
|
||||
|
||||
# Test render function from the console
|
||||
a <- 1
|
||||
r <- renderTriple({ a + 1 })
|
||||
a <- 2
|
||||
r()
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=quoToFunction]{quoToFunction()}}, \code{\link[=markRenderFunction]{markRenderFunction()}}.
|
||||
Implement render functions
|
||||
}
|
||||
|
||||
224
man/diskCache.Rd
224
man/diskCache.Rd
@@ -1,12 +1,12 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/deprecated.R
|
||||
% Please edit documentation in R/cache-disk.R
|
||||
\name{diskCache}
|
||||
\alias{diskCache}
|
||||
\title{Create disk cache (deprecated)}
|
||||
\title{Create a disk cache object}
|
||||
\usage{
|
||||
diskCache(
|
||||
dir = NULL,
|
||||
max_size = 500 * 1024^2,
|
||||
max_size = 10 * 1024^2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
@@ -17,12 +17,12 @@ diskCache(
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{dir}{Directory to store files for the cache. If \code{NULL} (the default) it
|
||||
will create and use a temporary directory.}
|
||||
\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. The default is 1 gigabyte.}
|
||||
\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.}
|
||||
@@ -32,25 +32,215 @@ 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.}
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{destroy_on_finalize}{If \code{TRUE}, then when the cache_disk object is
|
||||
\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 when \code{get(key)} is called but the key is not
|
||||
present in the cache. The default is a \code{\link[cachem:reexports]{key_missing()}} object. It is
|
||||
actually an expression that is evaluated each time there is a cache miss.
|
||||
See section Missing keys for more information.}
|
||||
\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]{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}{Deprecated.}
|
||||
\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{stderr()} or
|
||||
\code{stdout()}.}
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
Create disk cache (deprecated)
|
||||
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}.
|
||||
}
|
||||
\keyword{internal}
|
||||
\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]{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]{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]{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 check that the returned object is not a
|
||||
\code{key_missing()} object, using \code{is.key_missing()}. 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}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ calling environment two steps back.}
|
||||
}
|
||||
\description{
|
||||
This is to be called from another function, because it will attempt to get
|
||||
an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
|
||||
recommended to use \code{\link[=quoToFunction]{quoToFunction()}} instead.
|
||||
an unquoted expression from two calls back.
|
||||
}
|
||||
\details{
|
||||
If expr is a quoted expression, then this just converts it to a function.
|
||||
|
||||
@@ -8,7 +8,7 @@ getCurrentTheme()
|
||||
}
|
||||
\value{
|
||||
If called at render-time (i.e., inside a \code{\link[htmltools:tagFunction]{htmltools::tagFunction()}}),
|
||||
and \code{\link[=bootstrapLib]{bootstrapLib()}}'s \code{theme} has been set to a \code{\link[bslib:bs_theme]{bslib::bs_theme()}}
|
||||
and \code{\link[=bootstrapLib]{bootstrapLib()}}'s \code{theme} has been set to a \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}}
|
||||
object, then this returns the \code{theme}. Otherwise, this returns \code{NULL}.
|
||||
}
|
||||
\description{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
\alias{icon}
|
||||
\title{Create an icon}
|
||||
\usage{
|
||||
icon(name, class = NULL, lib = "font-awesome", ...)
|
||||
icon(name, class = NULL, lib = "font-awesome")
|
||||
}
|
||||
\arguments{
|
||||
\item{name}{Name of icon. Icons are drawn from the
|
||||
@@ -20,8 +20,6 @@ in icon names (i.e. the "fa-calendar" icon should be referred to as
|
||||
supported styles).}
|
||||
|
||||
\item{lib}{Icon library to use ("font-awesome" or "glyphicon")}
|
||||
|
||||
\item{...}{Arguments passed to the \verb{<i>} tag of \link[htmltools:builder]{htmltools::tags}}
|
||||
}
|
||||
\value{
|
||||
An icon element
|
||||
|
||||
@@ -35,8 +35,7 @@ the name of the calling function.}
|
||||
}
|
||||
\description{
|
||||
Installs an expression in the given environment as a function, and registers
|
||||
debug hooks so that breakpoints may be set in the function. Note: as of
|
||||
Shiny 1.6.0, it is recommended to use \code{\link[=quoToFunction]{quoToFunction()}} instead.
|
||||
debug hooks so that breakpoints may be set in the function.
|
||||
}
|
||||
\details{
|
||||
This function can replace \code{exprToFunction} as follows: we may use
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
\alias{markRenderFunction}
|
||||
\title{Mark a function as a render function}
|
||||
\usage{
|
||||
markRenderFunction(
|
||||
uiFunc,
|
||||
renderFunc,
|
||||
outputArgs = list(),
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
)
|
||||
markRenderFunction(uiFunc, renderFunc, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{uiFunc}{A function that renders Shiny UI. Must take a single argument:
|
||||
@@ -21,42 +14,19 @@ an output ID.}
|
||||
slot.}
|
||||
|
||||
\item{outputArgs}{A list of arguments to pass to the \code{uiFunc}. Render
|
||||
functions should include \code{outputArgs = list()} in their own parameter list,
|
||||
and pass through the value to \code{markRenderFunction}, to allow app authors to
|
||||
customize outputs. (Currently, this is only supported for dynamically
|
||||
generated UIs, such as those created by Shiny code snippets embedded in R
|
||||
Markdown documents).}
|
||||
|
||||
\item{cacheHint}{One of \code{"auto"}, \code{FALSE}, or some other information to
|
||||
identify this instance for caching using \code{\link[=bindCache]{bindCache()}}. If \code{"auto"}, it
|
||||
will try to automatically infer caching information. If \code{FALSE}, do not
|
||||
allow caching for the object. Some render functions (such as \link{renderPlot})
|
||||
contain internal state that makes them unsuitable for caching.}
|
||||
|
||||
\item{cacheWriteHook}{Used if the render function is passed to \code{bindCache()}.
|
||||
This is an optional callback function to invoke before saving the value
|
||||
from the render function to the cache. This function must accept one
|
||||
argument, the value returned from \code{renderFunc}, and should return the value
|
||||
to store in the cache.}
|
||||
|
||||
\item{cacheReadHook}{Used if the render function is passed to \code{bindCache()}.
|
||||
This is an optional callback function to invoke after reading a value from
|
||||
the cache (if there is a cache hit). The function will be passed one
|
||||
argument, the value retrieved from the cache. This can be useful when some
|
||||
side effect needs to occur for a render function to behave correctly. For
|
||||
example, some render functions call \code{\link[=createWebDependency]{createWebDependency()}} so that Shiny
|
||||
is able to serve JS and CSS resources.}
|
||||
functions should include \code{outputArgs = list()} in their own parameter
|
||||
list, and pass through the value to \code{markRenderFunction}, to allow
|
||||
app authors to customize outputs. (Currently, this is only supported for
|
||||
dynamically generated UIs, such as those created by Shiny code snippets
|
||||
embedded in R Markdown documents).}
|
||||
}
|
||||
\value{
|
||||
The \code{renderFunc} function, with annotations.
|
||||
}
|
||||
\description{
|
||||
Should be called by implementers of \code{renderXXX} functions in order to mark
|
||||
their return values as Shiny render functions, and to provide a hint to Shiny
|
||||
regarding what UI function is most commonly used with this type of render
|
||||
function. This can be used in R Markdown documents to create complete output
|
||||
widgets out of just the render function.
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=createRenderFunction]{createRenderFunction()}}, \code{\link[=quoToFunction]{quoToFunction()}}
|
||||
Should be called by implementers of \code{renderXXX} functions in order to
|
||||
mark their return values as Shiny render functions, and to provide a hint to
|
||||
Shiny regarding what UI function is most commonly used with this type of
|
||||
render function. This can be used in R Markdown documents to create complete
|
||||
output widgets out of just the render function.
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/deprecated.R
|
||||
% Please edit documentation in R/cache-memory.R
|
||||
\name{memoryCache}
|
||||
\alias{memoryCache}
|
||||
\title{Create memory cache (deprecated)}
|
||||
\title{Create a memory cache object}
|
||||
\usage{
|
||||
memoryCache(
|
||||
max_size = 200 * 1024^2,
|
||||
max_size = 10 * 1024^2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
@@ -17,7 +17,7 @@ memoryCache(
|
||||
\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. The default is 1 gigabyte.}
|
||||
\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.}
|
||||
@@ -27,20 +27,178 @@ 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.}
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{missing}{A value to return when \code{get(key)} is called but the key is not
|
||||
present in the cache. The default is a \code{\link[cachem:reexports]{key_missing()}} object. It is
|
||||
actually an expression that is evaluated each time there is a cache miss.
|
||||
See section Missing keys for more information.}
|
||||
\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]{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}{Deprecated.}
|
||||
\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{stderr()} or
|
||||
\code{stdout()}.}
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
Create memory cache (deprecated)
|
||||
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}.
|
||||
}
|
||||
\keyword{internal}
|
||||
\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]{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]{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]{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]{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}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ observe(
|
||||
x,
|
||||
env = parent.frame(),
|
||||
quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
suspended = FALSE,
|
||||
priority = 0,
|
||||
@@ -29,8 +28,6 @@ non-reactive expression.}
|
||||
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{...}{Not used.}
|
||||
|
||||
\item{label}{A label for the observer, useful for debugging.}
|
||||
|
||||
\item{suspended}{If \code{TRUE}, start the observer in a suspended state. If
|
||||
|
||||
@@ -12,7 +12,6 @@ observeEvent(
|
||||
event.quoted = FALSE,
|
||||
handler.env = parent.frame(),
|
||||
handler.quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
suspended = FALSE,
|
||||
priority = 0,
|
||||
@@ -30,7 +29,6 @@ eventReactive(
|
||||
event.quoted = FALSE,
|
||||
value.env = parent.frame(),
|
||||
value.quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
ignoreNULL = TRUE,
|
||||
@@ -64,8 +62,6 @@ 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{...}{Currently not used.}
|
||||
|
||||
\item{label}{A label for the observer or reactive, useful for debugging.}
|
||||
|
||||
\item{suspended}{If \code{TRUE}, start the observer in a suspended state. If
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{quoToFunction}
|
||||
\alias{quoToFunction}
|
||||
\title{Convert a quosure to a function for a Shiny render function}
|
||||
\usage{
|
||||
quoToFunction(q, label, ..stacktraceon = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{q}{A quosure.}
|
||||
|
||||
\item{label}{A label for the object to be shown in the debugger. Defaults to
|
||||
the name of the calling function.}
|
||||
|
||||
\item{..stacktraceon}{Advanced use only. For stack manipulation purposes; see
|
||||
\code{\link[=stacktrace]{stacktrace()}}.}
|
||||
}
|
||||
\description{
|
||||
This takes a quosure and label, and wraps them into a function that should be
|
||||
passed to \code{\link[=createRenderFunction]{createRenderFunction()}} or \code{\link[=markRenderFunction]{markRenderFunction()}}.
|
||||
}
|
||||
\details{
|
||||
This function was added in Shiny 1.6.0. Previously, it was recommended to use
|
||||
\code{\link[=installExprFunction]{installExprFunction()}} or \code{\link[=exprToFunction]{exprToFunction()}} in render functions, but now we
|
||||
recommend using \code{\link[=quoToFunction]{quoToFunction()}}, because it does not require \code{env} and
|
||||
\code{quoted} arguments -- that information is captured by quosures provided by
|
||||
\pkg{rlang}.
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=createRenderFunction]{createRenderFunction()}} for example usage.
|
||||
}
|
||||
@@ -9,7 +9,6 @@ reactive(
|
||||
x,
|
||||
env = parent.frame(),
|
||||
quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE
|
||||
@@ -29,8 +28,6 @@ non-reactive expression.}
|
||||
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{...}{Not used.}
|
||||
|
||||
\item{label}{A label for the reactive expression, useful for debugging.}
|
||||
|
||||
\item{domain}{See \link{domains}.}
|
||||
|
||||
@@ -33,9 +33,10 @@ 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[cachem:cache_disk]{cachem::cache_disk()}}. See the Cache Scoping section for more information.}
|
||||
\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]{diskCache()}}. See the Cache Scoping section for more
|
||||
information.}
|
||||
|
||||
\item{...}{Arguments to be passed through to \code{\link[grDevices:png]{grDevices::png()}}.
|
||||
These can be used to set the width, height, background color, etc.}
|
||||
@@ -55,8 +56,7 @@ interactive R Markdown document.}
|
||||
\code{sizePolicy}.}
|
||||
}
|
||||
\description{
|
||||
Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
|
||||
this is a shortcut for using \code{\link[=bindCache]{bindCache()}} with \code{\link[=renderPlot]{renderPlot()}}.
|
||||
Renders a reactive plot, with plot images cached to disk.
|
||||
}
|
||||
\details{
|
||||
\code{expr} is an expression that generates a plot, similar to that in
|
||||
@@ -97,6 +97,97 @@ 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]{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]{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]{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]{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]{memoryCache()}} or \code{\link[=diskCache]{diskCache()}}, and pass it
|
||||
as the \code{cache} argument of \code{renderCachedPlot}.
|
||||
}
|
||||
|
||||
\section{Interactive plots}{
|
||||
|
||||
|
||||
@@ -192,7 +283,7 @@ shinyApp(
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = cachem::cache_mem()
|
||||
cache = memoryCache()
|
||||
)
|
||||
output$plot2 <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
@@ -201,7 +292,7 @@ shinyApp(
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = cachem::cache_mem()
|
||||
cache = memoryCache()
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -212,22 +303,22 @@ shinyApp(
|
||||
# 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 = cachem::cache_mem(max_size = 20e6, max_age = 3600))
|
||||
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 = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
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 = cachem::cache_disk("./myapp-cache"))
|
||||
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 = cachem::cache_mem(max_size = 5e6))
|
||||
shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
...,
|
||||
@@ -238,8 +329,7 @@ server <- function(input, output, session) {
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
See \code{\link[=renderPlot]{renderPlot()}} for the regular, non-cached version of this
|
||||
function. It can be used with \code{\link[=bindCache]{bindCache()}} to get the same effect as
|
||||
\code{renderCachedPlot()}. For more about configuring caches, see
|
||||
\code{\link[cachem:cache_mem]{cachem::cache_mem()}} and \code{\link[cachem:cache_disk]{cachem::cache_disk()}}.
|
||||
See \code{\link[=renderPlot]{renderPlot()}} for the regular, non-cached version of
|
||||
this function. For more about configuring caches, see
|
||||
\code{\link[=memoryCache]{memoryCache()}} and \code{\link[=diskCache]{diskCache()}}.
|
||||
}
|
||||
|
||||
@@ -171,18 +171,6 @@ possible to specify by name which values to return by providing a
|
||||
character vector, as in \code{input=c("x", "y")}. The format can be
|
||||
"rds" or "json".
|
||||
}
|
||||
\item{setCurrentTheme(theme)}{
|
||||
Sets the current \code{\link[=bootstrapLib]{bootstrapLib()}} theme, which updates the value of
|
||||
\code{\link[=getCurrentTheme]{getCurrentTheme()}}, invalidates \code{session$getCurrentTheme()}, and calls
|
||||
function(s) registered with \code{\link[=registerThemeDependency]{registerThemeDependency()}} with provided
|
||||
\code{theme}. If those function calls return \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}s with
|
||||
\code{stylesheet}s, then those stylesheets are "refreshed" (i.e., the new
|
||||
stylesheets are inserted on the page and the old ones are disabled and
|
||||
removed).
|
||||
}
|
||||
\item{getCurrentTheme()}{
|
||||
A reactive read of the current \code{\link[=bootstrapLib]{bootstrapLib()}} theme.
|
||||
}
|
||||
}
|
||||
\description{
|
||||
Shiny server functions can optionally include \code{session} as a parameter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/deprecated.R
|
||||
% Please edit documentation in R/utils.R
|
||||
\name{shinyDeprecated}
|
||||
\alias{shinyDeprecated}
|
||||
\title{Print message for deprecated functions in Shiny}
|
||||
|
||||
@@ -147,7 +147,6 @@ can be set globally with \code{options()} or locally (for a single app) with
|
||||
\code{shinyOptions()}.
|
||||
|
||||
\describe{ \item{cache}{A caching object that will be used by
|
||||
\code{\link[=renderCachedPlot]{renderCachedPlot()}}. If not specified, a \code{\link[cachem:cache_mem]{cachem::cache_mem()}} will be
|
||||
used.} }
|
||||
\code{\link[=renderCachedPlot]{renderCachedPlot()}}. If not specified, a \code{\link[=memoryCache]{memoryCache()}} will be used.} }
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,17 @@ var browser = (function() {
|
||||
}
|
||||
|
||||
// Detect IE information
|
||||
var ua = window.navigator.userAgent;
|
||||
var isIE = /MSIE|Trident/.test(ua);
|
||||
var isIE = (navigator.appName === 'Microsoft Internet Explorer');
|
||||
|
||||
function getIEVersion() {
|
||||
var msie = ua.indexOf('MSIE ');
|
||||
if (isIE && msie > 0) {
|
||||
// IE 10 or older => return version number
|
||||
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
|
||||
var rv = -1;
|
||||
if (isIE) {
|
||||
var ua = navigator.userAgent;
|
||||
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
|
||||
if (re.exec(ua) !== null)
|
||||
rv = parseFloat(RegExp.$1);
|
||||
}
|
||||
var trident = ua.indexOf('Trident/');
|
||||
if (trident > 0) {
|
||||
// IE 11 => return version number
|
||||
var rv = ua.indexOf('rv:');
|
||||
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
|
||||
}
|
||||
return -1;
|
||||
return rv;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -107,76 +107,23 @@ function renderDependency(dep) {
|
||||
}
|
||||
|
||||
if (dep.stylesheet) {
|
||||
var links = $.map(asArray(dep.stylesheet), function(stylesheet) {
|
||||
return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
|
||||
var stylesheets = $.map(asArray(dep.stylesheet), function(stylesheet) {
|
||||
var this_href = href + "/" + encodeURI(stylesheet);
|
||||
// If a styleSheet with this href is already active, then disable it
|
||||
var re = new RegExp(this_href + "$");
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var h = document.styleSheets[i].href;
|
||||
if (!h) continue;
|
||||
if (restyle && h.match(re)) document.styleSheets[i].disabled = true;
|
||||
}
|
||||
// Force client to re-request the stylesheet if we've already seen it
|
||||
// (without this unique param, the request might get cached)
|
||||
if (restyle) {
|
||||
this_href = this_href + "?restyle=" + new Date().getTime();
|
||||
}
|
||||
return $("<link rel='stylesheet' type='text/css'>").attr("href", this_href);
|
||||
});
|
||||
|
||||
if (!restyle) {
|
||||
$head.append(links);
|
||||
} else {
|
||||
$.map(links, function(link) {
|
||||
// Find any document.styleSheets that match this link's href
|
||||
// so we can remove it after bringing in the new stylesheet
|
||||
var oldSheet = findSheet(link.attr("href"));
|
||||
// Add a timestamp to the href to prevent caching
|
||||
var href = link.attr("href") + "?restyle=" + new Date().getTime();
|
||||
// Use inline <style> approach for IE, otherwise use the more elegant
|
||||
// <link> -based approach
|
||||
if (browser.isIE) {
|
||||
refreshStyle(href, oldSheet);
|
||||
} else {
|
||||
link.attr("href", href);
|
||||
// Once the new <link> is loaded, schedule the old <link> to be removed
|
||||
// on the next tick which is needed to avoid FOUC
|
||||
link.attr("onload", () => {
|
||||
setTimeout(() => removeSheet(oldSheet), 500);
|
||||
});
|
||||
$head.append(link);
|
||||
}
|
||||
});
|
||||
|
||||
// Once the new styles are applied, CSS values that are accessible server-side
|
||||
// (e.g., getCurrentOutputInfo(), output visibility, etc) may become outdated.
|
||||
// At the time of writing, that means we need to do sendImageSize() &
|
||||
// sendOutputHiddenState() again, which can be done by re-binding.
|
||||
/* global Shiny */
|
||||
var bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
|
||||
setTimeout(() => bindDebouncer.normalCall(), 100);
|
||||
|
||||
// This inline <style> based approach works for IE11
|
||||
function refreshStyle(href, oldSheet) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', href);
|
||||
xhr.onload = function() {
|
||||
var id = "shiny_restyle_" + href.split("?restyle")[0].replace(/\W/g, '_');
|
||||
var oldStyle = $head.find("style#" + id);
|
||||
var newStyle = $("<style>").attr("id", id).html(xhr.responseText);
|
||||
$head.append(newStyle);
|
||||
setTimeout(() => oldStyle.remove(), 500);
|
||||
setTimeout(() => removeSheet(oldSheet), 500);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function findSheet(href) {
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var sheet = document.styleSheets[i];
|
||||
// The sheet's href is a full URL
|
||||
if (typeof sheet.href === "string" && sheet.href.indexOf(href) > -1) {
|
||||
return sheet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeSheet(sheet) {
|
||||
if (!sheet) return;
|
||||
sheet.disabled = true;
|
||||
if (browser.isIE) sheet.cssText = "";
|
||||
$(sheet.ownerNode).remove();
|
||||
}
|
||||
|
||||
}
|
||||
$head.append(stylesheets);
|
||||
}
|
||||
|
||||
if (dep.script && !restyle) {
|
||||
|
||||
@@ -149,7 +149,7 @@ function makeBlob(parts) {
|
||||
|
||||
function pixelRatio() {
|
||||
if (window.devicePixelRatio) {
|
||||
return Math.round(window.devicePixelRatio * 100) / 100;
|
||||
return window.devicePixelRatio;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Submodule tests/testthat/apps deleted from c471e6449e
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
|
||||
# Note that there are some tests for bindEvent() and caching in
|
||||
# test-with-cache.R.
|
||||
|
||||
test_that("bindEvent and observers", {
|
||||
trigger <- reactiveVal(1)
|
||||
val <- reactiveVal(10)
|
||||
|
||||
vals <- numeric()
|
||||
o <- bindEvent(
|
||||
trigger(),
|
||||
x = observe({
|
||||
vals <<- c(vals, val())
|
||||
})
|
||||
)
|
||||
|
||||
flushReact()
|
||||
expect_identical(vals, 10)
|
||||
|
||||
# Changing val has no effect
|
||||
val(20)
|
||||
flushReact()
|
||||
expect_identical(vals, 10)
|
||||
|
||||
# Changing trigger causes the observer to execute
|
||||
trigger(2)
|
||||
flushReact()
|
||||
expect_identical(vals, c(10, 20))
|
||||
|
||||
trigger(3)
|
||||
flushReact()
|
||||
expect_identical(vals, c(10, 20, 20))
|
||||
})
|
||||
|
||||
|
||||
test_that("bindEvent alters observers in place", {
|
||||
v <- reactiveVal(1)
|
||||
o <- observe({ v() })
|
||||
o1 <- bindEvent(o, v())
|
||||
|
||||
# o and o1 are the same object
|
||||
expect_identical(o, o1)
|
||||
|
||||
# Can't call bindEvent twice on an observer
|
||||
expect_error(bindEvent(o, v()))
|
||||
})
|
||||
|
||||
|
||||
test_that("ignoreNULL works", {
|
||||
n <- 0
|
||||
observe({ n <<- n+1 }) %>% bindEvent(NULL, ignoreNULL = FALSE)
|
||||
flushReact()
|
||||
expect_identical(n, 1)
|
||||
|
||||
n <- 0
|
||||
observe({ n <<- n+1 }) %>% bindEvent(NULL, ignoreNULL = TRUE)
|
||||
flushReact()
|
||||
expect_identical(n, 0)
|
||||
|
||||
# Two NULLs in the `...` get aggregated into a list, so the result is not
|
||||
# NULL.
|
||||
n <- 0
|
||||
observe({ n <<- n+1 }) %>% bindEvent(NULL, NULL, ignoreNULL = TRUE)
|
||||
flushReact()
|
||||
expect_identical(n, 1)
|
||||
})
|
||||
|
||||
|
||||
test_that("once=TRUE works", {
|
||||
n <- 0
|
||||
v <- reactiveVal(1)
|
||||
observe({ n <<- n + 1 }) %>% bindEvent(v(), once = FALSE)
|
||||
flushReact()
|
||||
expect_identical(n, 1)
|
||||
v(2)
|
||||
flushReact()
|
||||
expect_identical(n, 2)
|
||||
|
||||
n <- 0
|
||||
v <- reactiveVal(1)
|
||||
observe({ n <<- n + v() }) %>% bindEvent(v(), once = TRUE)
|
||||
flushReact()
|
||||
expect_identical(n, 1)
|
||||
v(2)
|
||||
flushReact()
|
||||
expect_identical(n, 1)
|
||||
})
|
||||
103
tests/testthat/test-cache.R
Normal file
103
tests/testthat/test-cache.R
Normal file
@@ -0,0 +1,103 @@
|
||||
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_equal(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))
|
||||
})
|
||||
|
||||
# Issue #3033
|
||||
test_that("DiskCache: pruning respects both max_n and max_size", {
|
||||
d <- diskCache(max_n = 3, max_size = 200)
|
||||
# Set some values. Use rnorm so that object size is large; a simple vector
|
||||
# like 1:100 will be stored very efficiently by R's ALTREP, and won't exceed
|
||||
# the max_size. We want each of these objects to exceed max_size so that
|
||||
# they'll be pruned.
|
||||
d$set("a", rnorm(100))
|
||||
d$set("b", rnorm(100))
|
||||
d$set("c", rnorm(100))
|
||||
d$set("d", rnorm(100))
|
||||
d$set("e", rnorm(100))
|
||||
Sys.sleep(0.1) # For systems that have low mtime resolution.
|
||||
d$set("f", 1) # This object is small and shouldn't be pruned.
|
||||
d$prune()
|
||||
expect_identical(d$keys(), "f")
|
||||
})
|
||||
|
||||
|
||||
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))
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
test_that("hybrid_chain preserves visibility", {
|
||||
expect_identical(
|
||||
hybrid_chain(1, withVisible),
|
||||
list(value = 1, visible = TRUE)
|
||||
)
|
||||
|
||||
expect_identical(
|
||||
hybrid_chain(invisible(1), withVisible),
|
||||
list(value = 1, visible = FALSE)
|
||||
)
|
||||
|
||||
expect_identical(
|
||||
hybrid_chain(1, identity, withVisible),
|
||||
list(value = 1, visible = TRUE)
|
||||
)
|
||||
|
||||
expect_identical(
|
||||
hybrid_chain(invisible(1), identity, withVisible),
|
||||
list(value = 1, visible = FALSE)
|
||||
)
|
||||
|
||||
expect_identical(
|
||||
hybrid_chain(1, function(x) invisible(x), withVisible),
|
||||
list(value = 1, visible = FALSE)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
test_that("hybrid_chain preserves visibility - async", {
|
||||
res <- NULL
|
||||
hybrid_chain(
|
||||
promise_resolve(1),
|
||||
function(value) res <<- withVisible(value)
|
||||
)
|
||||
later::run_now()
|
||||
expect_identical(res, list(value = 1, visible = TRUE))
|
||||
|
||||
res <- NULL
|
||||
hybrid_chain(
|
||||
promise_resolve(invisible(1)),
|
||||
function(value) res <<- withVisible(value)
|
||||
)
|
||||
later::run_now()
|
||||
expect_identical(res, list(value = 1, visible = FALSE))
|
||||
|
||||
res <- NULL
|
||||
hybrid_chain(
|
||||
promise_resolve(1),
|
||||
identity,
|
||||
function(value) res <<- withVisible(value)
|
||||
)
|
||||
for (i in 1:2) later::run_now()
|
||||
expect_identical(res, list(value = 1, visible = TRUE))
|
||||
|
||||
res <- NULL
|
||||
hybrid_chain(
|
||||
promise_resolve(invisible(1)),
|
||||
identity,
|
||||
function(value) res <<- withVisible(value)
|
||||
)
|
||||
for (i in 1:2) later::run_now()
|
||||
expect_identical(res, list(value = 1, visible = FALSE))
|
||||
|
||||
res <- NULL
|
||||
hybrid_chain(
|
||||
promise_resolve(1),
|
||||
function(x) invisible(x),
|
||||
function(value) res <<- withVisible(value)
|
||||
)
|
||||
for (i in 1:2) later::run_now()
|
||||
expect_identical(res, list(value = 1, visible = FALSE))
|
||||
|
||||
|
||||
})
|
||||
@@ -56,25 +56,22 @@ test_that("reactivePoll supported", {
|
||||
expect_equal(i, 2)
|
||||
})
|
||||
|
||||
# `renderCachedPlot()` is now implemented with `bindCache()`, and won't work by
|
||||
# calling `f(session, "name")`, because the key computation function is not
|
||||
# called with session and name.
|
||||
# test_that("renderCachedPlot supported", {
|
||||
# session <- MockShinySession$new()
|
||||
# isolate({
|
||||
# # renderCachedPlot is sensitive to having the cache set for it before entering.
|
||||
# origCache <- getShinyOption("cache")
|
||||
# shinyOptions(cache = cachem::cache_mem())
|
||||
# on.exit(shinyOptions(cache = origCache), add=TRUE)
|
||||
#
|
||||
# p <- renderCachedPlot({ plot(1,1) }, { Sys.time() })
|
||||
# plt <- p(session, "name")
|
||||
#
|
||||
# # Should have a size defined
|
||||
# expect_equal(plt$coordmap$dims$width, 692) #FIXME: why isn't this respecting the clientdata sizes?
|
||||
# expect_equal(plt$coordmap$dims$height, 400)
|
||||
# })
|
||||
# })
|
||||
test_that("renderCachedPlot supported", {
|
||||
session <- MockShinySession$new()
|
||||
isolate({
|
||||
# renderCachedPlot is sensitive to having the cache set for it before entering.
|
||||
origCache <- getShinyOption("cache")
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
on.exit(shinyOptions(cache = origCache), add=TRUE)
|
||||
|
||||
p <- renderCachedPlot({ plot(1,1) }, { Sys.time() })
|
||||
plt <- p(session, "name")
|
||||
|
||||
# Should have a size defined
|
||||
expect_equal(plt$coordmap$dims$width, 692) #FIXME: why isn't this respecting the clientdata sizes?
|
||||
expect_equal(plt$coordmap$dims$height, 400)
|
||||
})
|
||||
})
|
||||
|
||||
test_that("renderDataTable supported", {
|
||||
session <- MockShinySession$new()
|
||||
|
||||
@@ -659,115 +659,6 @@ test_that("suspended/resumed observers run at most once", {
|
||||
})
|
||||
|
||||
|
||||
test_that("reactive() accepts injected quosures", {
|
||||
# Normal usage - no quosures
|
||||
a <- 1
|
||||
f <- reactive({ a + 10 })
|
||||
a <- 2
|
||||
expect_identical(isolate(f()), 12)
|
||||
|
||||
# quosures can be used in reactive()
|
||||
a <- 1
|
||||
f <- reactive({ rlang::eval_tidy(rlang::quo(!!a + 10)) })
|
||||
a <- 2
|
||||
expect_identical(isolate(f()), 12)
|
||||
|
||||
# inject() with quosures
|
||||
a <- 1
|
||||
exp <- rlang::quo(a + 10)
|
||||
f <- inject(reactive(!!exp))
|
||||
a <- 2
|
||||
expect_identical(isolate(f()), 12)
|
||||
|
||||
# inject() with !!!
|
||||
a <- 1
|
||||
exp <- list(rlang::quo(a + 10))
|
||||
f <- inject(reactive(!!!exp))
|
||||
a <- 2
|
||||
expect_identical(isolate(f()), 12)
|
||||
|
||||
# inject() with captured environment
|
||||
a <- 1
|
||||
exp <- local({
|
||||
q <- rlang::quo(a + 10)
|
||||
a <- 2
|
||||
q
|
||||
})
|
||||
f <- inject(reactive(!! exp ))
|
||||
a <- 3
|
||||
expect_identical(isolate(f()), 12)
|
||||
|
||||
# inject() with nested quosures
|
||||
a <- 1
|
||||
y <- quo(a)
|
||||
exp <- quo(!!y + 10)
|
||||
a <- 2
|
||||
f <- inject(reactive(!! exp ))
|
||||
a <- 3
|
||||
expect_identical(isolate(f()), 13)
|
||||
})
|
||||
|
||||
test_that("observe() accepts injected quosures", {
|
||||
# Normal usage - no quosures
|
||||
val <- NULL
|
||||
a <- 1
|
||||
observe({ val <<- a + 10 })
|
||||
a <- 2
|
||||
flushReact()
|
||||
expect_identical(val, 12)
|
||||
|
||||
# quosures can be used in reactive()
|
||||
val <- NULL
|
||||
a <- 1
|
||||
f <- observe({ val <<- rlang::eval_tidy(rlang::quo(!!a + 10)) })
|
||||
a <- 2
|
||||
flushReact()
|
||||
expect_identical(val, 12)
|
||||
|
||||
# inject() with quosures
|
||||
val <- NULL
|
||||
a <- 1
|
||||
exp <- rlang::quo(val <<- a + 10)
|
||||
f <- inject(observe(!!exp))
|
||||
a <- 2
|
||||
flushReact()
|
||||
expect_identical(val, 12)
|
||||
|
||||
# inject() with !!!
|
||||
val <- NULL
|
||||
a <- 1
|
||||
exp <- list(quo(val <<- a + 10))
|
||||
f <- inject(observe(!!!exp))
|
||||
a <- 2
|
||||
flushReact()
|
||||
expect_identical(val, 12)
|
||||
|
||||
# inject() with captured environment
|
||||
val <- NULL
|
||||
a <- 1
|
||||
exp <- local({
|
||||
q <- rlang::quo(val <<- a + 10)
|
||||
a <- 2
|
||||
q
|
||||
})
|
||||
f <- inject(observe(!! exp ))
|
||||
a <- 3
|
||||
flushReact()
|
||||
expect_identical(val, 12)
|
||||
|
||||
# inject() with nested quosures
|
||||
val <- NULL
|
||||
a <- 1
|
||||
y <- quo(a)
|
||||
exp <- rlang::quo(val <<- !!y + 10)
|
||||
a <- 2
|
||||
f <- inject(observe(!!exp))
|
||||
a <- 3
|
||||
flushReact()
|
||||
expect_identical(val, 13)
|
||||
})
|
||||
|
||||
|
||||
test_that("reactive() accepts quoted and unquoted expressions", {
|
||||
vals <- reactiveValues(A=1)
|
||||
|
||||
@@ -793,12 +684,10 @@ test_that("reactive() accepts quoted and unquoted expressions", {
|
||||
expect_true(is.function(isolate(fun())))
|
||||
|
||||
|
||||
# Check that environment is correct - parent of parent environment should be
|
||||
# this one. Note that rlang::as_function() injects an intermediate
|
||||
# environment.
|
||||
# Check that environment is correct - parent environment should be this one
|
||||
this_env <- environment()
|
||||
fun <- reactive(environment())
|
||||
expect_identical(isolate(parent.env(parent.env(fun()))), this_env)
|
||||
expect_identical(isolate(parent.env(fun())), this_env)
|
||||
|
||||
# Sanity check: environment structure for a reactive() should be the same as for
|
||||
# a normal function
|
||||
@@ -837,13 +726,12 @@ test_that("observe() accepts quoted and unquoted expressions", {
|
||||
expect_equal(valB, 4)
|
||||
|
||||
|
||||
# Check that environment is correct - parent of parent environment should be
|
||||
# this one. rlang::as_function() injects one intermediate env.
|
||||
# Check that environment is correct - parent environment should be this one
|
||||
this_env <- environment()
|
||||
inside_env <- NULL
|
||||
fun <- observe(inside_env <<- environment())
|
||||
flushReact()
|
||||
expect_identical(parent.env(parent.env(inside_env)), this_env)
|
||||
expect_identical(parent.env(inside_env), this_env)
|
||||
})
|
||||
|
||||
test_that("Observer priorities are respected", {
|
||||
@@ -1544,64 +1432,3 @@ test_that("reactiveTimer prefers session$scheduleTask", {
|
||||
}
|
||||
expect_gt(called, 0)
|
||||
})
|
||||
|
||||
|
||||
test_that("Reactive expression visibility", {
|
||||
res <- NULL
|
||||
rv <- reactive(1)
|
||||
o <- observe({
|
||||
res <<- withVisible(rv())
|
||||
})
|
||||
flushReact()
|
||||
expect_identical(res, list(value = 1, visible = TRUE))
|
||||
|
||||
|
||||
res <- NULL
|
||||
rv <- reactive(invisible(1))
|
||||
o <- observe({
|
||||
res <<- withVisible(rv())
|
||||
})
|
||||
flushReact()
|
||||
expect_identical(res, list(value = 1, visible = FALSE))
|
||||
|
||||
# isolate
|
||||
expect_identical(
|
||||
withVisible(isolate(1)),
|
||||
list(value = 1, visible = TRUE)
|
||||
)
|
||||
expect_identical(
|
||||
withVisible(isolate(invisible(1))),
|
||||
list(value = 1, visible = FALSE)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
test_that("Reactive expression labels", {
|
||||
r <- list()
|
||||
|
||||
# Automatic label
|
||||
r$x <- reactive({
|
||||
a+1;b+ 2
|
||||
})
|
||||
# Printed output - uses expression, not `label`
|
||||
expect_identical(
|
||||
capture.output(print(r$x)),
|
||||
c("reactive({", " a + 1", " b + 2", "}) ")
|
||||
)
|
||||
# Label used for debugging
|
||||
expect_identical(
|
||||
as.character(attr(r$x, "observable")$.label),
|
||||
"r$x"
|
||||
)
|
||||
|
||||
# With explicit label
|
||||
r$y <- reactive({ a+1;b+ 2 }, label = "hello")
|
||||
expect_identical(
|
||||
capture.output(print(r$y)),
|
||||
c("reactive({", " a + 1", " b + 2", "}) ")
|
||||
)
|
||||
expect_identical(
|
||||
as.character(attr(r$y, "observable")$.label),
|
||||
"hello"
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
test_that("Render functions correctly handle quosures", {
|
||||
# Normally, quosures are not unwrapped at creation time.
|
||||
# However, using inject() will make it unwrap at creation time.
|
||||
|
||||
a <- 1
|
||||
r1 <- inject(renderText({ !!a }))
|
||||
r2 <- renderText({ eval_tidy(quo(!!a)) })
|
||||
a <- 2
|
||||
expect_identical(r1(), "1")
|
||||
expect_identical(r2(), "2")
|
||||
|
||||
a <- 1
|
||||
r1 <- inject(renderPrint({ !!a }))
|
||||
r2 <- renderPrint({ eval_tidy(quo(!!a)) })
|
||||
a <- 2
|
||||
expect_identical(r1(), "[1] 1")
|
||||
expect_identical(r2(), "[1] 2")
|
||||
|
||||
a <- 1
|
||||
r1 <- inject(renderUI({ tags$p(!!a) }))
|
||||
r2 <- renderUI({ eval_tidy(quo(tags$p(!!a))) })
|
||||
a <- 2
|
||||
res1 <- r1(shinysession = MockShinySession$new(), name = "foo")
|
||||
expect_identical(as.character(res1$html), "<p>1</p>")
|
||||
res2 <- r2(shinysession = MockShinySession$new(), name = "foo")
|
||||
expect_identical(as.character(res2$html), "<p>2</p>")
|
||||
|
||||
a <- 1
|
||||
r1 <- inject(renderTable({ pressure[!!a, ] }, digits = 1))
|
||||
r2 <- renderTable({ eval_tidy(quo(pressure[!!a, ])) }, digits = 1)
|
||||
a <- 2
|
||||
expect_true(grepl("0\\.0", r1()))
|
||||
expect_true(grepl("20\\.0", r2()))
|
||||
})
|
||||
@@ -86,7 +86,7 @@ test_that("integration tests", {
|
||||
"renderTable", "func", "force", "withVisible", "withCallingHandlers",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
|
||||
"tryCatch", "do", "hybrid_chain", "renderFunc", "renderTable({ C() }, server = FALSE)",
|
||||
"tryCatch", "do", "hybrid_chain", "origRenderFunc", "renderTable({ C() }, server = FALSE)",
|
||||
"..stacktraceon..", "contextFunc", "env$runWith", "force",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"withReactiveDomain", "domain$wrapSync", "promises::with_promise_domain",
|
||||
|
||||
@@ -566,7 +566,7 @@ test_that("validates server function", {
|
||||
# bindings are considered `fields`.
|
||||
get_mocked_publics <- function(instance, generator) {
|
||||
publics <- ls(instance, all.names = TRUE)
|
||||
actives <- names(generator$active) %||% character(0)
|
||||
actives <- names(generator$active) %OR% character(0)
|
||||
# Active bindings are considered fields.
|
||||
methods_or_fields <- publics[!(publics %in% actives)]
|
||||
methods <- character(0)
|
||||
|
||||
@@ -165,47 +165,20 @@ test_that("app template works with runTests", {
|
||||
random_folder <- paste0("shinyAppTemplate-", paste0(combo, collapse = "_"))
|
||||
tempTemplateDir <- file.path(tempdir(), random_folder)
|
||||
shinyAppTemplate(tempTemplateDir, combo)
|
||||
on.exit(unlink(tempTemplateDir, recursive = TRUE), add = TRUE)
|
||||
on.exit(unlink(tempTemplateDir, recursive = TRUE))
|
||||
|
||||
if (any(c("all", "shinytest", "testthat") %in% combo)) {
|
||||
|
||||
# suppress all output messages
|
||||
ignore <- capture.output({
|
||||
# do not let an error here stop this test
|
||||
# string comparisons will be made below
|
||||
test_result <- try({
|
||||
runTests(tempTemplateDir)
|
||||
}, silent = TRUE)
|
||||
})
|
||||
|
||||
expected_test_output <- paste0(
|
||||
c(
|
||||
"Shiny App Test Results",
|
||||
"* Success",
|
||||
expect_output(
|
||||
print(runTests(tempTemplateDir)),
|
||||
paste0(
|
||||
"Shiny App Test Results\\n\\* Success",
|
||||
if (any(c("all", "shinytest") %in% combo))
|
||||
paste0(" - ", file.path(random_folder, "tests", "shinytest.R")),
|
||||
paste0("\\n - ", file.path(random_folder, "tests", "shinytest\\.R")),
|
||||
if (any(c("all", "testthat") %in% combo))
|
||||
paste0(" - ", file.path(random_folder, "tests", "testthat.R"))
|
||||
),
|
||||
collapse = "\n"
|
||||
paste0("\\n - ", file.path(random_folder, "tests", "testthat\\.R"))
|
||||
)
|
||||
)
|
||||
|
||||
test_output <- paste0(
|
||||
capture.output({
|
||||
print(test_result)
|
||||
}),
|
||||
collapse = "\n"
|
||||
)
|
||||
|
||||
if (identical(expected_test_output, test_output)) {
|
||||
testthat::succeed()
|
||||
} else {
|
||||
# be very verbose in the error output to help find root cause of failure
|
||||
cat("\nrunTests() output:\n", test_output, "\n\n")
|
||||
cat("Expected print output:\n", expected_test_output, "\n")
|
||||
testthat::fail(paste0("runTests() output for '", random_folder, "' failed. Received:\n", test_output, "\n"))
|
||||
}
|
||||
|
||||
} else {
|
||||
expect_error(
|
||||
runTests(tempTemplateDir)
|
||||
|
||||
@@ -205,39 +205,3 @@ test_that("Application directories are identified", {
|
||||
normalizePath(dirname(tests), winslash = "/")
|
||||
)
|
||||
})
|
||||
|
||||
test_that("dateYMD works", {
|
||||
expect_identical(dateYMD("2020-01-14"),"2020-01-14")
|
||||
expect_identical(dateYMD("2020/01/14"),"2020-01-14")
|
||||
expect_identical(
|
||||
dateYMD(c("2020-01-14", "2019-11-05")),
|
||||
c("2020-01-14", "2019-11-05")
|
||||
)
|
||||
expect_identical(
|
||||
dateYMD(c("2020/01/14", "2019/11/05")),
|
||||
c("2020-01-14", "2019-11-05")
|
||||
)
|
||||
|
||||
expect_identical(
|
||||
expect_warning(dateYMD("")),
|
||||
""
|
||||
)
|
||||
expect_identical(
|
||||
expect_warning(dateYMD(c(NA))),
|
||||
NA
|
||||
)
|
||||
expect_identical(
|
||||
expect_warning(dateYMD(c("", NA))),
|
||||
c("", NA)
|
||||
)
|
||||
|
||||
# If there are any bad values, the entire thing goes through unchanged
|
||||
expect_identical(
|
||||
expect_warning(dateYMD(c("2019/11/05", NA))),
|
||||
c("2019/11/05", NA)
|
||||
)
|
||||
expect_identical(
|
||||
expect_warning(dateYMD(c("2019/11/05", ""))),
|
||||
c("2019/11/05", "")
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,16 +2,16 @@ diff --git a/inst/www/shared/datepicker/scss/build3.scss b/inst/www/shared/datep
|
||||
index b5388654..0dac6a62 100644
|
||||
--- a/inst/www/shared/datepicker/scss/build3.scss
|
||||
+++ b/inst/www/shared/datepicker/scss/build3.scss
|
||||
@@ -6,32 +6,33 @@
|
||||
@@ -6,32 +6,48 @@
|
||||
//
|
||||
// Variables and mixins copied from Bootstrap 3.3.5
|
||||
|
||||
|
||||
-// Variables
|
||||
-$gray: lighten(#000, 33.5%); // #555
|
||||
-$gray-light: lighten(#000, 46.7%); // #777
|
||||
-$gray-lighter: lighten(#000, 93.5%); // #eee
|
||||
+// These are BS3 variables that are used in datepicker3.scss. So, when compiling against
|
||||
+// a BS3 bslib theme, these variables should already be defined. Here we set
|
||||
+// a BS3 bootstraplib theme, these variables should already be defined. Here we set
|
||||
+// *defaults* for these variables based on BS4 variables, so this scss can work for
|
||||
+// both BS3 and BS4
|
||||
+$gray: mix($body-bg, $body-color, 33.5%) !default;
|
||||
@@ -29,13 +29,21 @@ index b5388654..0dac6a62 100644
|
||||
+//$line-height-base: 1.428571429;
|
||||
+//$btn-link-disabled-color: $gray-light;
|
||||
+//$dropdown-bg: #fff;
|
||||
|
||||
|
||||
-$brand-primary: darken(#428bca, 6.5%); // #337ab7
|
||||
|
||||
+// Setup BS4 style color contrasting
|
||||
+$yiq-contrasted-threshold: 150 !default;
|
||||
+$yiq-text-dark: #212529 !default;
|
||||
+$yiq-text-light: #fff !default;
|
||||
|
||||
-$btn-primary-color: #fff;
|
||||
-$btn-primary-bg: $brand-primary;
|
||||
-$btn-primary-border: darken($btn-primary-bg, 5%);
|
||||
|
||||
+@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
|
||||
+ $r: red($color);
|
||||
+ $g: green($color);
|
||||
+ $b: blue($color);
|
||||
|
||||
-$btn-link-disabled-color: $gray-light;
|
||||
-
|
||||
-$state-info-bg: #d9edf7;
|
||||
@@ -45,12 +53,19 @@ index b5388654..0dac6a62 100644
|
||||
-
|
||||
-$dropdown-bg: #fff;
|
||||
-$dropdown-border: rgba(0,0,0,.15);
|
||||
|
||||
|
||||
+ $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
+ @if ($yiq >= $yiq-contrasted-threshold) {
|
||||
+ @return $dark;
|
||||
+ } @else {
|
||||
+ @return $light;
|
||||
+ }
|
||||
+}
|
||||
|
||||
-// Mixins
|
||||
+@mixin button-variant($background, $border) {
|
||||
+ $color: color-contrast($background);
|
||||
|
||||
+ $color: color-yiq($background);
|
||||
|
||||
-// Button variants
|
||||
-@mixin button-variant($color, $background, $border) {
|
||||
color: $color;
|
||||
@@ -79,7 +94,7 @@ index b5388654..0dac6a62 100644
|
||||
- border-color: darken($border, 12%);
|
||||
+ background-color: mix($background, $color, 90%);
|
||||
+ border-color: mix($border, $color, 88%);
|
||||
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.focus {
|
||||
@@ -99,7 +114,7 @@ index 3d1621e0..3e031b31 100644
|
||||
}
|
||||
&.day:hover,
|
||||
&.focused {
|
||||
+ color: color-contrast($gray-lighter);
|
||||
+ color: color-yiq($gray-lighter);
|
||||
background: $gray-lighter;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -110,7 +125,7 @@ index 3d1621e0..3e031b31 100644
|
||||
- @include button-variant(#000, $highlighted-bg, darken($highlighted-bg, 20%));
|
||||
+ @include button-variant($highlighted-bg, darken($highlighted-bg, 20%));
|
||||
border-radius: 0;
|
||||
|
||||
|
||||
&.focused {
|
||||
@@ -109,7 +110,7 @@
|
||||
}
|
||||
@@ -118,7 +133,7 @@ index 3d1621e0..3e031b31 100644
|
||||
$today-bg: lighten(orange, 30%);
|
||||
- @include button-variant(#000, $today-bg, darken($today-bg, 20%));
|
||||
+ @include button-variant($today-bg, darken($today-bg, 20%));
|
||||
|
||||
|
||||
&.focused {
|
||||
background: darken($today-bg, 10%);
|
||||
@@ -123,7 +124,7 @@
|
||||
@@ -128,7 +143,7 @@ index 3d1621e0..3e031b31 100644
|
||||
- @include button-variant(#000, $range-bg, darken($range-bg, 20%));
|
||||
+ @include button-variant($range-bg, darken($range-bg, 20%));
|
||||
border-radius: 0;
|
||||
|
||||
|
||||
&.focused {
|
||||
@@ -138,7 +139,7 @@
|
||||
}
|
||||
@@ -136,7 +151,7 @@ index 3d1621e0..3e031b31 100644
|
||||
$range-highlighted-bg: mix($state-info-bg, $gray-lighter, 50%);
|
||||
- @include button-variant(#000, $range-highlighted-bg, darken($range-highlighted-bg, 20%));
|
||||
+ @include button-variant($range-highlighted-bg, darken($range-highlighted-bg, 20%));
|
||||
|
||||
|
||||
&.focused {
|
||||
background: darken($range-highlighted-bg, 10%);
|
||||
@@ -152,7 +153,7 @@
|
||||
@@ -145,7 +160,7 @@ index 3d1621e0..3e031b31 100644
|
||||
$range-today-bg: mix(orange, $gray-lighter, 50%);
|
||||
- @include button-variant(#000, $range-today-bg, darken($range-today-bg, 20%));
|
||||
+ @include button-variant($range-today-bg, darken($range-today-bg, 20%));
|
||||
|
||||
|
||||
&.disabled,
|
||||
&.disabled:active {
|
||||
@@ -162,12 +163,12 @@
|
||||
@@ -167,7 +182,7 @@ index 3d1621e0..3e031b31 100644
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&.focused {
|
||||
+ color: color-contrast($gray-lighter);
|
||||
+ color: color-yiq($gray-lighter);
|
||||
background: $gray-lighter;
|
||||
}
|
||||
&.disabled,
|
||||
@@ -185,8 +200,8 @@ index 3d1621e0..3e031b31 100644
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
- background: $gray-lighter;
|
||||
+ color: color-contrast($gray-lighter);
|
||||
+ color: color-yiq($gray-lighter);
|
||||
+ background: $gray-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
diff --git a/inst/www/shared/datepicker/scss/datepicker3.scss b/inst/www/shared/datepicker/scss/datepicker3.scss
|
||||
index 3e031b31..758f7301 100644
|
||||
--- a/inst/www/shared/datepicker/scss/datepicker3.scss
|
||||
+++ b/inst/www/shared/datepicker/scss/datepicker3.scss
|
||||
@@ -1,5 +1,15 @@
|
||||
+// Both BS3 and BS4 define a border radius mixin, but just in case
|
||||
+// we're trying to compile this without bootstrapSass
|
||||
+@mixin border-radius-shim($radius) {
|
||||
+ @if mixin-exists("border-radius") {
|
||||
+ @include border-radius($radius);
|
||||
+ } @else {
|
||||
+ border-radius: $radius;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
.datepicker {
|
||||
- border-radius: $border-radius-base;
|
||||
+ @include border-radius-shim($border-radius-base);
|
||||
&-inline {
|
||||
width: 220px;
|
||||
}
|
||||
@@ -64,7 +74,7 @@
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
- border-radius: 4px;
|
||||
+ @include border-radius-shim(4px);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
@@ -179,7 +189,7 @@
|
||||
float: left;
|
||||
margin: 1%;
|
||||
cursor: pointer;
|
||||
- border-radius: 4px;
|
||||
+ @include border-radius-shim(4px);
|
||||
&:hover,
|
||||
&.focused {
|
||||
color: color-contrast($gray-lighter);
|
||||
@@ -243,10 +253,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
input:first-child {
|
||||
- border-radius: 3px 0 0 3px;
|
||||
+ @include border-radius-shim(3px 0 0 3px);
|
||||
}
|
||||
input:last-child {
|
||||
- border-radius: 0 3px 3px 0;
|
||||
+ @include border-radius-shim(0 3px 3px 0);
|
||||
}
|
||||
.input-group-addon {
|
||||
width: auto;
|
||||
@@ -32,8 +32,7 @@ local({
|
||||
known_unindexed <- c("shiny-package", "stacktrace", "knitr_methods",
|
||||
"pageWithSidebar", "headerPanel", "shiny.appobj",
|
||||
"deprecatedReactives", "reexports", "makeReactiveBinding",
|
||||
"reactiveConsole", "registerThemeDependency",
|
||||
"memoryCache", "diskCache")
|
||||
"reactiveConsole")
|
||||
|
||||
## This test ensures that every documented topic is included in
|
||||
## staticdocs/index.r, unless explicitly waived by specifying it
|
||||
@@ -43,15 +42,12 @@ local({
|
||||
unknown <- setdiff(c(known_unindexed, indexed_topics), c(all_topics, reexports_man_file_names))
|
||||
|
||||
testthat::expect_equal(length(missing), 0,
|
||||
info = paste("Functions missing from ./tools/documentation/pkgdown.yml:\n",
|
||||
paste(" - ", missing, sep = "", collapse = "\n"),
|
||||
"\nPlease update ./tools/documentation/pkgdown.yml or ",
|
||||
"`known_unindexed` in ./tools/documentation/checkPkgdown.R",
|
||||
info = paste("Functions missing from _pkgdown.yml:\n",
|
||||
paste(" ", missing, sep = "", collapse = "\n"),
|
||||
sep = ""))
|
||||
testthat::expect_equal(length(unknown), 0,
|
||||
info = paste("Unrecognized functions in ./tools/documentation/pkgdown.yml:\n",
|
||||
paste(" - ", unknown, sep = "", collapse = "\n"),
|
||||
"\nPlease update ./tools/documentation/pkgdown.yml",
|
||||
info = paste("Unrecognized functions in _pkgdown.yml:\n",
|
||||
paste(" ", unknown, sep = "", collapse = "\n"),
|
||||
sep = ""))
|
||||
invisible(TRUE)
|
||||
})
|
||||
|
||||
@@ -118,8 +118,6 @@ reference:
|
||||
- observeEvent
|
||||
- reactiveVal
|
||||
- reactiveValues
|
||||
- bindCache
|
||||
- bindEvent
|
||||
- reactiveValuesToList
|
||||
- is.reactivevalues
|
||||
- isolate
|
||||
@@ -178,7 +176,6 @@ reference:
|
||||
- applyInputHandlers
|
||||
- exprToFunction
|
||||
- installExprFunction
|
||||
- quoToFunction
|
||||
- parseQueryString
|
||||
- getCurrentOutputInfo
|
||||
- getCurrentTheme
|
||||
@@ -194,6 +191,8 @@ reference:
|
||||
- shinyDeprecated
|
||||
- serverInfo
|
||||
- onStop
|
||||
- diskCache
|
||||
- memoryCache
|
||||
- httpResponse
|
||||
- key_missing
|
||||
- title: Plot interaction
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
diff --git a/inst/www/shared/ionrangeslider/scss/shiny.scss b/inst/www/shared/ionrangeslider/scss/shiny.scss
|
||||
new file mode 100644
|
||||
index 00000000..4a0fccc1
|
||||
index 00000000..3356cb6d
|
||||
--- /dev/null
|
||||
+++ b/inst/www/shared/ionrangeslider/scss/shiny.scss
|
||||
@@ -0,0 +1,174 @@
|
||||
@@ -0,0 +1,182 @@
|
||||
+/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
+© RStudio, Inc, 2014
|
||||
+© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
@@ -12,17 +12,25 @@ index 00000000..4a0fccc1
|
||||
+
|
||||
+@import "_base";
|
||||
+
|
||||
+// Both BS3 and BS4 define a border radius mixin, but just in case
|
||||
+// we're trying to compile this without bootstrapSass
|
||||
+@mixin border-radius-shim($radius) {
|
||||
+ @if mixin-exists("border-radius") {
|
||||
+ @include border-radius($radius);
|
||||
+////////////////////////////////////////////////////////////////////////////
|
||||
+// Setup BS4 style color contrasting
|
||||
+$yiq-contrasted-threshold: 150 !default;
|
||||
+$yiq-text-dark: #212529 !default;
|
||||
+$yiq-text-light: #fff !default;
|
||||
+
|
||||
+@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
|
||||
+ $r: red($color);
|
||||
+ $g: green($color);
|
||||
+ $b: blue($color);
|
||||
+
|
||||
+ $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
+
|
||||
+ @if ($yiq >= $yiq-contrasted-threshold) {
|
||||
+ @return $dark;
|
||||
+ } @else {
|
||||
+ border-radius: $radius;
|
||||
+ @return $light;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+
|
||||
+////////////////////////////////////////////////////////////////////////////
|
||||
+
|
||||
+// Re-define font-family on .irs to make it configurable
|
||||
@@ -64,7 +72,7 @@ index 00000000..4a0fccc1
|
||||
+ $minmax_line_height: 1.333 !default;
|
||||
+
|
||||
+ $fromto_bg_color: $accent !default;
|
||||
+ $fromto_color: color-contrast($fromto_bg_color) !default;
|
||||
+ $fromto_color: color-yiq($fromto_bg_color) !default;
|
||||
+ $fromto_font_size: 11px !default;
|
||||
+ $fromto_line_height: 1.333 !default;
|
||||
+
|
||||
@@ -85,7 +93,7 @@ index 00000000..4a0fccc1
|
||||
+ background: $line_bg;
|
||||
+ background-color: $line_bg_color;
|
||||
+ border: $line_border;
|
||||
+ @include border-radius-shim($line_height);
|
||||
+ border-radius: $line_height;
|
||||
+ }
|
||||
+
|
||||
+ .#{$name}-bar {
|
||||
@@ -96,7 +104,7 @@ index 00000000..4a0fccc1
|
||||
+ background: $bar_color;
|
||||
+
|
||||
+ &--single {
|
||||
+ @include border-radius-shim($line_height 0 0 $line_height);
|
||||
+ border-radius: $line_height 0 0 $line_height;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@@ -105,7 +113,7 @@ index 00000000..4a0fccc1
|
||||
+ top: 38px;
|
||||
+ height: 2px;
|
||||
+ background: rgba($fg, 0.3);
|
||||
+ @include border-radius-shim(5px);
|
||||
+ border-radius: 5px;
|
||||
+ }
|
||||
+ .lt-ie9 .#{$name}-shadow {
|
||||
+ filter: alpha(opacity=30);
|
||||
@@ -120,7 +128,7 @@ index 00000000..4a0fccc1
|
||||
+ border: $handle_border;
|
||||
+ background-color: $handle_color;
|
||||
+ box-shadow: $handle_box_shadow;
|
||||
+ @include border-radius-shim($handle_width);
|
||||
+ border-radius: $handle_width;
|
||||
+
|
||||
+ &.state_hover,
|
||||
+ &:hover {
|
||||
@@ -135,7 +143,7 @@ index 00000000..4a0fccc1
|
||||
+ color: $minmax_text_color;
|
||||
+ text-shadow: none;
|
||||
+ background-color: $minmax_bg_color;
|
||||
+ @include border-radius-shim($custom_radius);
|
||||
+ border-radius: $custom_radius;
|
||||
+ font-size: $minmax_font_size;
|
||||
+ line-height: $minmax_line_height;
|
||||
+ }
|
||||
@@ -152,7 +160,7 @@ index 00000000..4a0fccc1
|
||||
+ text-shadow: none;
|
||||
+ padding: 1px 3px;
|
||||
+ background-color: $fromto_bg_color;
|
||||
+ @include border-radius-shim($custom_radius);
|
||||
+ border-radius: $custom_radius;
|
||||
+ font-size: $fromto_font_size;
|
||||
+ line-height: $fromto_line_height;
|
||||
+ }
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/inst/www/shared/selectize/scss/selectize.scss b/inst/www/shared/selectize/scss/selectize.scss
|
||||
index 240e3895..c20f529c 100644
|
||||
--- a/inst/www/shared/selectize/scss/selectize.scss
|
||||
+++ b/inst/www/shared/selectize/scss/selectize.scss
|
||||
@@ -57,9 +57,11 @@ $selectize-caret-margin: 0 2px 0 0 !default;
|
||||
$selectize-caret-margin-rtl: 0 4px 0 -2px !default;
|
||||
|
||||
@mixin selectize-border-radius($radii){
|
||||
- -webkit-border-radius: $radii;
|
||||
- -moz-border-radius: $radii;
|
||||
- border-radius: $radii;
|
||||
+ @if mixin-exists("border-radius") {
|
||||
+ @include border-radius($radii)
|
||||
+ } @else {
|
||||
+ border-radius: $radii;
|
||||
+ }
|
||||
}
|
||||
@mixin selectize-unselectable(){
|
||||
-webkit-user-select: none;
|
||||
@@ -75,19 +75,17 @@ for (patch in list.files(patch_dir, full.names = TRUE)) {
|
||||
|
||||
# Compile to CSS
|
||||
library(sass)
|
||||
library(bslib)
|
||||
library(bootstraplib)
|
||||
css_dir <- file.path(dest_dir, "css")
|
||||
dir.create(css_dir, recursive = TRUE)
|
||||
sass_partial(
|
||||
bs_sass(
|
||||
sass_file(file.path(dest_dir, "scss", "build3.scss")),
|
||||
bundle = bs_theme(),
|
||||
output = file.path(css_dir, "bootstrap-datepicker3.css"),
|
||||
write_attachments = FALSE
|
||||
theme = bs_theme(),
|
||||
output = file.path(css_dir, "bootstrap-datepicker3.css")
|
||||
)
|
||||
sass_partial(
|
||||
bs_sass(
|
||||
sass_file(file.path(dest_dir, "scss", "build3.scss")),
|
||||
bundle = bs_theme(),
|
||||
theme = bs_theme(),
|
||||
output = file.path(css_dir, "bootstrap-datepicker3.min.css"),
|
||||
options = sass_options(output_style = "compressed"),
|
||||
write_attachments = FALSE
|
||||
options = sass_options(output_style = "compressed")
|
||||
)
|
||||
|
||||
@@ -57,10 +57,7 @@ for (patch in list.files(patch_dir, full.names = TRUE)) {
|
||||
library(sass)
|
||||
dir.create(file.path(target, "css"))
|
||||
sass(
|
||||
list(
|
||||
sass::sass_file(system.file("sass-utils/color-contrast.scss", package = "bslib")),
|
||||
sass_file(file.path(target, "scss", "shiny.scss"))
|
||||
),
|
||||
sass_file(file.path(target, "scss", "shiny.scss")),
|
||||
output = file.path(target, "css", "ion.rangeSlider.css")
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user