mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
296 Commits
v1.0.5
...
cache-even
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f5f2396d | ||
|
|
598b48d078 | ||
|
|
205c35d5e5 | ||
|
|
bf0dd7d725 | ||
|
|
ba2b811172 | ||
|
|
be347c3ed4 | ||
|
|
c01abdb6a9 | ||
|
|
95a5a965a5 | ||
|
|
fc2849a8ff | ||
|
|
fcc900f3e0 | ||
|
|
9d0bcd5637 | ||
|
|
6ebbad5273 | ||
|
|
930459899a | ||
|
|
fe730e2d76 | ||
|
|
e58b2e9a47 | ||
|
|
719dbab0c2 | ||
|
|
86ea023e2e | ||
|
|
bc0fb3f44c | ||
|
|
6d37f6b4dd | ||
|
|
958ab85297 | ||
|
|
a23f973433 | ||
|
|
c124256bad | ||
|
|
f1b035bcca | ||
|
|
81cc7c591e | ||
|
|
a0ca560c3b | ||
|
|
d1f20a9c73 | ||
|
|
013059c5b9 | ||
|
|
fe6ad235ac | ||
|
|
67af26ffe6 | ||
|
|
0fce9de04f | ||
|
|
a8b8df21d6 | ||
|
|
ab2e304f02 | ||
|
|
574f2c53d4 | ||
|
|
bc85d812d2 | ||
|
|
364990a29f | ||
|
|
9ac9e36873 | ||
|
|
6745e09688 | ||
|
|
e758927c84 | ||
|
|
90fbf7d50f | ||
|
|
75f1ee0082 | ||
|
|
750aaf451a | ||
|
|
b44bfe9109 | ||
|
|
aa392f8563 | ||
|
|
ac7228f6c4 | ||
|
|
dcb12addaa | ||
|
|
ad398b5f8a | ||
|
|
803cb4806e | ||
|
|
1a468bbb61 | ||
|
|
c332c051f3 | ||
|
|
db48befcb7 | ||
|
|
b02edb05ac | ||
|
|
d7009fd1c8 | ||
|
|
ce3755676c | ||
|
|
db3c1b728d | ||
|
|
1761de4740 | ||
|
|
09d496925b | ||
|
|
3af5327f1c | ||
|
|
06cb14d7ec | ||
|
|
7be1a9d7fa | ||
|
|
95243fb35c | ||
|
|
26438a3979 | ||
|
|
28db097a71 | ||
|
|
76fdd8ae04 | ||
|
|
3a73bfb142 | ||
|
|
a24bdabf08 | ||
|
|
8815f293a2 | ||
|
|
9af2775539 | ||
|
|
ae5deae6e9 | ||
|
|
61c2126498 | ||
|
|
881fe0cfce | ||
|
|
a999bf389c | ||
|
|
ff3b97b630 | ||
|
|
639b520d39 | ||
|
|
19dc29ea17 | ||
|
|
97bebae8d7 | ||
|
|
cf534ce6da | ||
|
|
f25f691a55 | ||
|
|
cbebf8be7b | ||
|
|
165ce26b2f | ||
|
|
572c863bff | ||
|
|
d3c85d67b8 | ||
|
|
ff3434f77e | ||
|
|
762528c044 | ||
|
|
1891af0d4a | ||
|
|
583ad036f7 | ||
|
|
ac92bf98d4 | ||
|
|
fd90ff7ff7 | ||
|
|
d06dbbe5db | ||
|
|
bffc4995d7 | ||
|
|
4b8b406bed | ||
|
|
5641153272 | ||
|
|
08c6c7781f | ||
|
|
ad2ad391a7 | ||
|
|
caac88be0d | ||
|
|
10660aa373 | ||
|
|
cfaf97aee4 | ||
|
|
55f14576f0 | ||
|
|
4dca94ac99 | ||
|
|
14779d3d27 | ||
|
|
66d1e710b5 | ||
|
|
12ae3c17e9 | ||
|
|
36e4da0709 | ||
|
|
91631cb081 | ||
|
|
224f082e1f | ||
|
|
76b239a6ea | ||
|
|
cb476b510d | ||
|
|
334f233968 | ||
|
|
e1f21250b9 | ||
|
|
8d087e4f20 | ||
|
|
9e35e8c947 | ||
|
|
f98faef024 | ||
|
|
0f9346ead5 | ||
|
|
fc8118c694 | ||
|
|
026b7278c1 | ||
|
|
375a7e7e5c | ||
|
|
7a1aecb1a4 | ||
|
|
b3690e8680 | ||
|
|
97d490cfb4 | ||
|
|
2081dda6fc | ||
|
|
ea912fc50c | ||
|
|
b655fdf68f | ||
|
|
4749f46a4f | ||
|
|
f95bb9c82d | ||
|
|
6529529cdb | ||
|
|
3a2a3f21d4 | ||
|
|
631bc1c481 | ||
|
|
597af36759 | ||
|
|
691062f687 | ||
|
|
6651c4ea48 | ||
|
|
116559e5a0 | ||
|
|
7818e8ed64 | ||
|
|
2880391620 | ||
|
|
f742605a1b | ||
|
|
2afff67e89 | ||
|
|
fe7bd53250 | ||
|
|
6df3509869 | ||
|
|
062dc771aa | ||
|
|
9c3a0c86ca | ||
|
|
01b24e984c | ||
|
|
9dd4302fe9 | ||
|
|
c2f03aa833 | ||
|
|
2260459422 | ||
|
|
e838cc3fe9 | ||
|
|
74457b95e9 | ||
|
|
d5754515a6 | ||
|
|
4ed13c04f5 | ||
|
|
5a5294cc44 | ||
|
|
3a5d48ae7c | ||
|
|
ffe883ab72 | ||
|
|
31c4e0fdfe | ||
|
|
66f970e0bd | ||
|
|
07b223dcb0 | ||
|
|
f1e27b6ffb | ||
|
|
389463aea5 | ||
|
|
b11ab9a31c | ||
|
|
5fe85b07b7 | ||
|
|
3c7b1e7d21 | ||
|
|
c556cf1e69 | ||
|
|
722e5fb5f7 | ||
|
|
e90cc591b7 | ||
|
|
c555725201 | ||
|
|
cef1f3c7ee | ||
|
|
e5d1fa1ea4 | ||
|
|
3ccf2937b4 | ||
|
|
b7b696630f | ||
|
|
84aba546bc | ||
|
|
741236df56 | ||
|
|
e3584f0a61 | ||
|
|
432482c5a7 | ||
|
|
323ad46bba | ||
|
|
ace0fe1802 | ||
|
|
36f244fece | ||
|
|
99e5ef99ec | ||
|
|
d6d3ed5bbc | ||
|
|
49d09ecf30 | ||
|
|
c529a03096 | ||
|
|
101d9aa0fa | ||
|
|
b4864e1180 | ||
|
|
cba7304ab9 | ||
|
|
2d058b0519 | ||
|
|
eed9231884 | ||
|
|
5c84eaf2a5 | ||
|
|
2ef7226be0 | ||
|
|
e5d1c61cdf | ||
|
|
e635055ab8 | ||
|
|
d8d4e3b262 | ||
|
|
8f29543479 | ||
|
|
c11a8ea24b | ||
|
|
86646d7faa | ||
|
|
6e44915e08 | ||
|
|
f8b99cf4e9 | ||
|
|
0e7d6ff192 | ||
|
|
66501dac97 | ||
|
|
195907b2ec | ||
|
|
be11b44864 | ||
|
|
bc7cd21c13 | ||
|
|
0555cbdd28 | ||
|
|
97498451bb | ||
|
|
2e0d9b5475 | ||
|
|
62395f3103 | ||
|
|
6b31cd6aee | ||
|
|
e67a8ba369 | ||
|
|
133d301925 | ||
|
|
17c40a5d1d | ||
|
|
042211e5f6 | ||
|
|
d12830d700 | ||
|
|
b411c70280 | ||
|
|
2bc22cc7d5 | ||
|
|
b4c189c89b | ||
|
|
fe3f351a2d | ||
|
|
076be9cba7 | ||
|
|
f28dcd85fb | ||
|
|
8e0f17c9d7 | ||
|
|
d73817a0db | ||
|
|
11874db825 | ||
|
|
5d5a43ce90 | ||
|
|
75e548caab | ||
|
|
c901e7ba06 | ||
|
|
b1dc3dfca1 | ||
|
|
ce4ed20c69 | ||
|
|
d44df7f860 | ||
|
|
54353e0e1f | ||
|
|
1c042b6efb | ||
|
|
b8df1f29c4 | ||
|
|
18252f5b03 | ||
|
|
881370f284 | ||
|
|
35d1747bc3 | ||
|
|
91ac89a54e | ||
|
|
3c694d9bd9 | ||
|
|
6a78e9df77 | ||
|
|
078c6eb30a | ||
|
|
d35c6002a6 | ||
|
|
f23fc3beaa | ||
|
|
5a352e5ace | ||
|
|
27cae0065e | ||
|
|
50be2993fa | ||
|
|
d9ea15e9bc | ||
|
|
03b1d45d7e | ||
|
|
e48d6878c4 | ||
|
|
1a3b255848 | ||
|
|
f00aa94d7e | ||
|
|
f7980b19f4 | ||
|
|
6a1f9677a5 | ||
|
|
e844bb36a5 | ||
|
|
ae364adfc2 | ||
|
|
c14a382b90 | ||
|
|
da9c2beaaf | ||
|
|
a4a56476db | ||
|
|
39d3784b9b | ||
|
|
7d29df58f1 | ||
|
|
05aa413683 | ||
|
|
132f90f45b | ||
|
|
4526fd1917 | ||
|
|
2602dc15b0 | ||
|
|
2314f63424 | ||
|
|
c2410600ee | ||
|
|
f7e4702685 | ||
|
|
71682512c4 | ||
|
|
20b82fbf77 | ||
|
|
631f09847d | ||
|
|
671585f68a | ||
|
|
5feed888bb | ||
|
|
47bef0f1b0 | ||
|
|
c1dc662a40 | ||
|
|
16e1721fe8 | ||
|
|
f406e13600 | ||
|
|
9063133a7b | ||
|
|
3fbb436187 | ||
|
|
7c845d070b | ||
|
|
5e905aa73e | ||
|
|
e15654f265 | ||
|
|
369c067efc | ||
|
|
c037e69793 | ||
|
|
8c935ff44e | ||
|
|
74bf8b0554 | ||
|
|
6345972efe | ||
|
|
16242e87a1 | ||
|
|
8155320ba5 | ||
|
|
39a7f63972 | ||
|
|
7b72209277 | ||
|
|
cad20a0bfe | ||
|
|
ba8d79f202 | ||
|
|
176fe699b9 | ||
|
|
213ee7be13 | ||
|
|
48fd869c71 | ||
|
|
53e47484e2 | ||
|
|
dc18b20e5a | ||
|
|
b4c5debbdf | ||
|
|
771d3d52b9 | ||
|
|
2a53ac093d | ||
|
|
4fa2af72cc | ||
|
|
e512d3cd61 | ||
|
|
16b7ee3985 | ||
|
|
4f3d26c31b | ||
|
|
587bf94d69 | ||
|
|
33258da6c3 |
@@ -18,3 +18,5 @@
|
||||
^.*\.o$
|
||||
^appveyor\.yml$
|
||||
^revdep$
|
||||
^TODO-promises.md$
|
||||
^manualtests$
|
||||
|
||||
26
DESCRIPTION
26
DESCRIPTION
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.0.5
|
||||
Version: 1.1.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
|
||||
@@ -64,7 +64,8 @@ Depends:
|
||||
methods
|
||||
Imports:
|
||||
utils,
|
||||
httpuv (>= 1.3.5),
|
||||
grDevices,
|
||||
httpuv (>= 1.4.3.9001),
|
||||
mime (>= 0.3),
|
||||
jsonlite (>= 0.9.16),
|
||||
xtable,
|
||||
@@ -72,7 +73,11 @@ Imports:
|
||||
htmltools (>= 0.3.5),
|
||||
R6 (>= 2.0),
|
||||
sourcetools,
|
||||
tools
|
||||
later (>= 0.7.2),
|
||||
promises (>= 1.0.1),
|
||||
tools,
|
||||
crayon,
|
||||
rlang
|
||||
Suggests:
|
||||
datasets,
|
||||
Cairo (>= 1.5-5),
|
||||
@@ -84,18 +89,24 @@ Suggests:
|
||||
magrittr
|
||||
URL: http://shiny.rstudio.com
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Collate:
|
||||
Remotes:
|
||||
tidyverse/ggplot2,
|
||||
rstudio/httpuv
|
||||
Collate:
|
||||
'app.R'
|
||||
'bookmark-state-local.R'
|
||||
'stack.R'
|
||||
'bookmark-state.R'
|
||||
'bootstrap-layout.R'
|
||||
'globals.R'
|
||||
'conditions.R'
|
||||
'map.R'
|
||||
'globals.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'cache.R'
|
||||
'cache-context.R'
|
||||
'cache-disk.R'
|
||||
'cache-memory.R'
|
||||
'cache-utils.R'
|
||||
'diagnose.R'
|
||||
'fileupload.R'
|
||||
'graph.R'
|
||||
@@ -134,6 +145,7 @@ Collate:
|
||||
'priorityqueue.R'
|
||||
'progress.R'
|
||||
'react.R'
|
||||
'render-cached-plot.R'
|
||||
'render-plot.R'
|
||||
'render-table.R'
|
||||
'run-url.R'
|
||||
@@ -150,4 +162,4 @@ Collate:
|
||||
'test-export.R'
|
||||
'timer.R'
|
||||
'update-input.R'
|
||||
RoxygenNote: 6.0.1
|
||||
RoxygenNote: 6.1.0
|
||||
|
||||
15
NAMESPACE
15
NAMESPACE
@@ -25,6 +25,7 @@ S3method(as.tags,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(names,reactivevalues)
|
||||
S3method(print,key_missing)
|
||||
S3method(print,reactive)
|
||||
S3method(print,shiny.appobj)
|
||||
S3method(str,reactivevalues)
|
||||
@@ -59,6 +60,7 @@ export(code)
|
||||
export(column)
|
||||
export(conditionStackTrace)
|
||||
export(conditionalPanel)
|
||||
export(createRenderFunction)
|
||||
export(createWebDependency)
|
||||
export(dataTableOutput)
|
||||
export(dateInput)
|
||||
@@ -66,6 +68,7 @@ export(dateRangeInput)
|
||||
export(dblclickOpts)
|
||||
export(debounce)
|
||||
export(dialogViewer)
|
||||
export(diskCache)
|
||||
export(div)
|
||||
export(downloadButton)
|
||||
export(downloadHandler)
|
||||
@@ -89,6 +92,7 @@ export(fluidRow)
|
||||
export(formatStackTrace)
|
||||
export(freezeReactiveVal)
|
||||
export(freezeReactiveValue)
|
||||
export(getCurrentOutputInfo)
|
||||
export(getDefaultReactiveDomain)
|
||||
export(getQueryString)
|
||||
export(getShinyOption)
|
||||
@@ -120,6 +124,7 @@ export(insertTab)
|
||||
export(insertUI)
|
||||
export(installExprFunction)
|
||||
export(invalidateLater)
|
||||
export(is.key_missing)
|
||||
export(is.reactive)
|
||||
export(is.reactivevalues)
|
||||
export(is.shiny.appobj)
|
||||
@@ -127,6 +132,7 @@ export(is.singleton)
|
||||
export(isRunning)
|
||||
export(isTruthy)
|
||||
export(isolate)
|
||||
export(key_missing)
|
||||
export(knit_print.html)
|
||||
export(knit_print.reactive)
|
||||
export(knit_print.shiny.appobj)
|
||||
@@ -137,6 +143,7 @@ export(mainPanel)
|
||||
export(makeReactiveBinding)
|
||||
export(markRenderFunction)
|
||||
export(maskReactiveContext)
|
||||
export(memoryCache)
|
||||
export(modalButton)
|
||||
export(modalDialog)
|
||||
export(navbarMenu)
|
||||
@@ -188,6 +195,7 @@ export(removeModal)
|
||||
export(removeNotification)
|
||||
export(removeTab)
|
||||
export(removeUI)
|
||||
export(renderCachedPlot)
|
||||
export(renderDataTable)
|
||||
export(renderImage)
|
||||
export(renderPlot)
|
||||
@@ -225,6 +233,7 @@ export(showTab)
|
||||
export(sidebarLayout)
|
||||
export(sidebarPanel)
|
||||
export(singleton)
|
||||
export(sizeGrowthRatio)
|
||||
export(sliderInput)
|
||||
export(snapshotExclude)
|
||||
export(snapshotPreprocessInput)
|
||||
@@ -267,9 +276,13 @@ export(updateSliderInput)
|
||||
export(updateTabsetPanel)
|
||||
export(updateTextAreaInput)
|
||||
export(updateTextInput)
|
||||
export(updateVarSelectInput)
|
||||
export(updateVarSelectizeInput)
|
||||
export(urlModal)
|
||||
export(validate)
|
||||
export(validateCssUnit)
|
||||
export(varSelectInput)
|
||||
export(varSelectizeInput)
|
||||
export(verbatimTextOutput)
|
||||
export(verticalLayout)
|
||||
export(wellPanel)
|
||||
@@ -285,3 +298,5 @@ import(httpuv)
|
||||
import(methods)
|
||||
import(mime)
|
||||
import(xtable)
|
||||
importFrom(grDevices,dev.cur)
|
||||
importFrom(grDevices,dev.set)
|
||||
|
||||
107
NEWS.md
107
NEWS.md
@@ -1,3 +1,100 @@
|
||||
shiny 1.1.0.9000
|
||||
===========
|
||||
|
||||
## Full changelog
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Support for selecting variables of a data frame with the output values to be used within tidy evaluation. Added functions: `varSelectInput`, `varSelectizeInput`, `updateVarSelectInput`, `updateVarSelectizeInput`. ([#2091](https://github.com/rstudio/shiny/pull/2091))
|
||||
|
||||
* Addressed [#2042](https://github.com/rstudio/shiny/issues/2042): dates outside of `min`/`max` date range are now a lighter shade of grey to highlight the allowed range. ([#2087](https://github.com/rstudio/shiny/pull/2087))
|
||||
|
||||
* Fixed [#1933](https://github.com/rstudio/shiny/issues/1933): extended server-side selectize to lists and optgroups. ([#2102](https://github.com/rstudio/shiny/pull/2102))
|
||||
|
||||
* Fixed [#1935](https://github.com/rstudio/shiny/issues/1935): correctly returns plot coordinates when using outer margins. ([#2108](https://github.com/rstudio/shiny/pull/2108))
|
||||
|
||||
* Resolved [#2019](https://github.com/rstudio/shiny/issues/2019): `updateSliderInput` now changes the slider formatting if the input type changes. ([#2099](https://github.com/rstudio/shiny/pull/2099))
|
||||
|
||||
* Added namespace support when freezing reactiveValue keys. [#2080](https://github.com/rstudio/shiny/pull/2080)
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
* Addressed [#1864](https://github.com/rstudio/shiny/issues/1864) by changing `optgroup` documentation to use `list` instead of `c`. ([#2084](https://github.com/rstudio/shiny/pull/2084))
|
||||
|
||||
|
||||
shiny 1.1.0
|
||||
===========
|
||||
|
||||
This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more.
|
||||
|
||||
## Full changelog
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `extractStackTrace` and `formatStackTrace` are deprecated and will be removed in a future version of Shiny. As far as we can tell, nobody has been using these functions, and a refactor has made them vestigial; if you need this functionality, please file an issue.
|
||||
|
||||
### New features
|
||||
|
||||
* Support for asynchronous operations! Built-in render functions that expected a certain kind of object to be yielded from their `expr`, now generally can handle a promise for that kind of object. Reactive expressions and observers are now promise-aware as well. ([#1932](https://github.com/rstudio/shiny/pull/1932))
|
||||
|
||||
* Introduced two changes to the (undocumented but widely used) JavaScript function `Shiny.onInputChange(name, value)`. First, we changed the function name to `Shiny.setInputValue` (but don't worry--the old function name will continue to work). Second, until now, all calls to `Shiny.onInputChange(inputId, value)` have been "deduplicated"; that is, anytime an input is set to the same value it already has, the set is ignored. With Shiny v1.1, you can now add an options object as the third parameter: `Shiny.setInputValue("name", value, {priority: "event"})`. When the priority option is set to `"event"`, Shiny will always send the value and trigger reactivity, whether it is a duplicate or not. This closes [#928](https://github.com/rstudio/shiny/issues/928), which was the most upvoted open issue by far! Thanks, @daattali. ([#2018](https://github.com/rstudio/shiny/pull/2018))
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Addressed [#1978](https://github.com/rstudio/shiny/issues/1978): `shiny:value` is now triggered when duplicate output data is received from the server. (Thanks, @andrewsali! [#1999](https://github.com/rstudio/shiny/pull/1999))
|
||||
|
||||
* If a shiny output contains a css class of `shiny-report-size`, its container height and width are now reported in `session$clientData`. So, for an output with an id with `"myID"`, the height/width can be accessed via `session$clientData[['output_myID_height']]`/`session$clientData[['output_myID_width']]`. Addresses [#1980](https://github.com/rstudio/shiny/issues/1980). (Thanks, @cpsievert! [#1981](https://github.com/rstudio/shiny/pull/1981))
|
||||
|
||||
* Added a new `autoclose = TRUE` parameter to `dateInput()` and `dateRangeInput()`. This closed [#1969](https://github.com/rstudio/shiny/issues/1969) which was a duplicate of much older issue, [#173](https://github.com/rstudio/shiny/issues/173). The default value is `TRUE` since that seems to be the common use case. However, this will cause existing apps with date inputs (that update to this version of Shiny) to have the datepicker be immediately closed once a date is selected. For most apps, this is actually desired behavior; if you wish to keep the datepicker open until the user clicks out of it use `autoclose = FALSE`. ([#1987](https://github.com/rstudio/shiny/pull/1987))
|
||||
|
||||
* The version of Shiny is now accessible from Javascript, with `Shiny.version`. There is also a new function for comparing version strings, `Shiny.compareVersion()`. ([#1826](https://github.com/rstudio/shiny/pull/1826), [#1830](https://github.com/rstudio/shiny/pull/1830))
|
||||
|
||||
* Addressed [#1851](https://github.com/rstudio/shiny/issues/1851): Stack traces are now smaller in some places `do.call()` is used. ([#1856](https://github.com/rstudio/shiny/pull/1856))
|
||||
|
||||
* Stack traces have been improved, with more aggressive de-noising and support for deep stack traces (stitching together multiple stack traces that are conceptually part of the same async operation).
|
||||
|
||||
* Addressed [#1859](https://github.com/rstudio/shiny/issues/1859): Server-side selectize is now significantly faster. (Thanks to @dselivanov [#1861](https://github.com/rstudio/shiny/pull/1861))
|
||||
|
||||
* [#1989](https://github.com/rstudio/shiny/issues/1989): The server side of outputs can now be removed (e.g. `output$plot <- NULL`). This is not usually necessary but it does allow some objects to be garbage collected, which might matter if you are dynamically creating and destroying many outputs. (Thanks, @mmuurr! [#2011](https://github.com/rstudio/shiny/pull/2011))
|
||||
|
||||
* Removed the (ridiculously outdated) "experimental feature" tag from the reference documentation for `renderUI`. ([#2036](https://github.com/rstudio/shiny/pull/2036))
|
||||
|
||||
* Addressed [#1907](https://github.com/rstudio/shiny/issues/1907): the `ignoreInit` argument was first added only to `observeEvent`. Later, we also added it to `eventReactive`, but forgot to update the documentation. Now done, thanks [@flo12392](https://github.com/flo12392)! ([#2036](https://github.com/rstudio/shiny/pull/2036))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed [#1006](https://github.com/rstudio/shiny/issues/1006): Slider inputs sometimes showed too many digits. ([#1956](https://github.com/rstudio/shiny/pull/1956))
|
||||
|
||||
* Fixed [#1958](https://github.com/rstudio/shiny/issues/1958): Slider inputs previously displayed commas after a decimal point. ([#1960](https://github.com/rstudio/shiny/pull/1960))
|
||||
|
||||
* The internal `URLdecode()` function previously was a copy of `httpuv::decodeURIComponent()`, assigned at build time; now it invokes the httpuv function at run time.
|
||||
|
||||
* Fixed [#1840](https://github.com/rstudio/shiny/issues/1840): with the release of Shiny 1.0.5, we accidently changed the relative positioning of the icon and the title text in `navbarMenu`s and `tabPanel`s. This fix reverts this behavior back (i.e. the icon should be to the left of the text and/or the downward arrow in case of `navbarMenu`s). ([#1848](https://github.com/rstudio/shiny/pull/1848))
|
||||
|
||||
* Fixed [#1600](https://github.com/rstudio/shiny/issues/1600): URL-encoded bookmarking did not work with sliders that had dates or date-times. ([#1961](https://github.com/rstudio/shiny/pull/1961))
|
||||
|
||||
* Fixed [#1962](https://github.com/rstudio/shiny/issues/1962): [File dragging and dropping](https://blog.rstudio.com/2017/08/15/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). ([#2005](https://github.com/rstudio/shiny/pull/2005))
|
||||
|
||||
* Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. ([#1968](https://github.com/rstudio/shiny/pull/1968))
|
||||
|
||||
* Changed script tags in reactlog ([inst/www/reactive-graph.html](https://github.com/rstudio/shiny/blob/master/inst/www/reactive-graph.html)) from HTTP to HTTPS in order to avoid mixed content blocking by most browsers. (Thanks, @jekriske-lilly! [#1844](https://github.com/rstudio/shiny/pull/1844))
|
||||
|
||||
* Addressed [#1784](https://github.com/rstudio/shiny/issues/1784): `runApp()` will avoid port 6697, which is considered unsafe by Chrome.
|
||||
|
||||
* Fixed [#2000](https://github.com/rstudio/shiny/issues/2000): Implicit calls to `xxxOutput` not working inside modules. (Thanks, @GregorDeCillia! [#2010](https://github.com/rstudio/shiny/pull/2010))
|
||||
|
||||
* Fixed [#2021](https://github.com/rstudio/shiny/issues/2021): Memory leak with `reactiveTimer` and `invalidateLater`. ([#2022](https://github.com/rstudio/shiny/pull/2022))
|
||||
|
||||
### Library updates
|
||||
|
||||
* Updated to ion.rangeSlider 2.2.0. ([#1955](https://github.com/rstudio/shiny/pull/1955))
|
||||
|
||||
|
||||
## Known issues
|
||||
|
||||
In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)).
|
||||
|
||||
|
||||
shiny 1.0.5
|
||||
===========
|
||||
|
||||
@@ -209,7 +306,7 @@ Now there's an official way to slow down reactive values and expressions that in
|
||||
### Minor new features and improvements
|
||||
|
||||
* Addressed [#1486](https://github.com/rstudio/shiny/issues/1486) by adding a new argument to `observeEvent` and `eventReactive`, called `ignoreInit` (defaults to `FALSE` for backwards compatibility). When set to `TRUE`, the action (i.e. the second argument: `handlerExpr` and `valueExpr`, respectively) will not be triggered when the observer/reactive is first created/initialized. In other words, `ignoreInit = TRUE` ensures that the `observeEvent` (or `eventReactive`) is *never* run right away. For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
|
||||
|
||||
|
||||
* Added a new argument to `observeEvent` called `once`. When set to `TRUE`, it results in the observer being destroyed (stop observing) after the first time that `handlerExpr` is run (i.e. `once = TRUE` guarantees that the observer only runs, at most, once). For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
|
||||
|
||||
* Addressed [#1358](https://github.com/rstudio/shiny/issues/1358): more informative error message when calling `runApp()` inside of an app's app.R (or inside ui.R or server.R). ([#1482](https://github.com/rstudio/shiny/pull/1482))
|
||||
@@ -608,7 +705,7 @@ shiny 0.12.1
|
||||
shiny 0.12.0
|
||||
============
|
||||
|
||||
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
|
||||
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
|
||||
|
||||
### JSON serialization
|
||||
|
||||
@@ -699,13 +796,13 @@ Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version,
|
||||
### Known issues for migration
|
||||
|
||||
* In Bootstrap 3, images in `<img>` tags are no longer automatically scaled to the width of their container. If you use `img()` in your UI code, or `<img>` tags in your raw HTML source, it's possible that they will be too large in the new version of Shiny. To address this you can add the `img-responsive` class:
|
||||
|
||||
|
||||
```r
|
||||
img(src = "picture.png", class = "img-responsive")
|
||||
```
|
||||
|
||||
|
||||
The R code above will generate the following HTML:
|
||||
|
||||
|
||||
```html
|
||||
<img src="picture.png" class="img-responsive">
|
||||
```
|
||||
|
||||
5
R/app.R
5
R/app.R
@@ -381,9 +381,10 @@ print.shiny.appobj <- function(x, ...) {
|
||||
c("port", "launch.browser", "host", "quiet",
|
||||
"display.mode", "test.mode")]
|
||||
|
||||
args <- c(list(x), opts)
|
||||
# Quote x and put runApp in quotes so that there's a nicer stack trace (#1851)
|
||||
args <- c(list(quote(x)), opts)
|
||||
|
||||
do.call(runApp, args)
|
||||
do.call("runApp", args)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
|
||||
@@ -451,11 +451,21 @@ hasCurrentRestoreContext <- function() {
|
||||
restoreCtxStack$size() > 0
|
||||
}
|
||||
|
||||
# Call to access the current restore context
|
||||
# Call to access the current restore context. First look on the restore
|
||||
# context stack, and if not found, then see if there's one on the current
|
||||
# reactive domain. In practice, the only time there will be a restore context
|
||||
# on the stack is when executing the UI function; when executing server code,
|
||||
# the restore context will be attached to the domain/session.
|
||||
getCurrentRestoreContext <- function() {
|
||||
ctx <- restoreCtxStack$peek()
|
||||
if (is.null(ctx)) {
|
||||
stop("No restore context found")
|
||||
domain <- getDefaultReactiveDomain()
|
||||
|
||||
if (is.null(domain) || is.null(domain$restoreContext)) {
|
||||
stop("No restore context found")
|
||||
}
|
||||
|
||||
ctx <- domain$restoreContext
|
||||
}
|
||||
ctx
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
|
||||
}
|
||||
|
||||
if (length(flex) > length(children)) {
|
||||
flex <- flex[1:length(children)]
|
||||
flex <- flex[seq_along(children)]
|
||||
}
|
||||
|
||||
# The dimension along the main axis
|
||||
|
||||
@@ -883,8 +883,8 @@ buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
|
||||
tags$a(href = "#",
|
||||
class = "dropdown-toggle", `data-toggle` = "dropdown",
|
||||
`data-value` = divTag$menuName,
|
||||
divTag$title, tags$b(class = "caret"),
|
||||
getIcon(iconClass = divTag$iconClass)
|
||||
getIcon(iconClass = divTag$iconClass),
|
||||
divTag$title, tags$b(class = "caret")
|
||||
),
|
||||
tabset$navList # inner tabPanels items
|
||||
)
|
||||
@@ -899,8 +899,8 @@ buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
|
||||
href = paste("#", tabId, sep = ""),
|
||||
`data-toggle` = "tab",
|
||||
`data-value` = divTag$attribs$`data-value`,
|
||||
divTag$attribs$title,
|
||||
getIcon(iconClass = divTag$attribs$`data-icon-class`)
|
||||
getIcon(iconClass = divTag$attribs$`data-icon-class`),
|
||||
divTag$attribs$title
|
||||
)
|
||||
)
|
||||
# if this tabPanel is selected item, mark it active
|
||||
|
||||
561
R/cache-disk.R
Normal file
561
R/cache-disk.R
Normal file
@@ -0,0 +1,561 @@
|
||||
#' Create a disk cache object
|
||||
#'
|
||||
#' A disk cache object is a key-value store that saves the values as files in a
|
||||
#' directory on disk. Objects can be stored and retrieved using the \code{get()}
|
||||
#' and \code{set()} methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
#' and \code{evict}.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing Keys:
|
||||
#'
|
||||
#' The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
#' when \code{get()} is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
#' object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for \code{missing} that takes one argument, the key, and also use
|
||||
#' \code{exec_missing=TRUE}.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for \code{missing}, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
#' argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that \code{get()} returns a sentinel value
|
||||
#' to represent a cache miss, then \code{set} will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time \code{get()} encounters missing
|
||||
#' key. If the function returns a value, then \code{get()} will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then \code{get()} will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to \code{missing}, and use
|
||||
#' \code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls \code{get()} should be wrapped with
|
||||
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
#' manually by calling \code{prune()}.
|
||||
#'
|
||||
#' The disk cache will throttle the pruning so that it does not happen on
|
||||
#' every call to \code{set()}, because the filesystem operations for checking
|
||||
#' the status of files can be slow. Instead, it will prune once in every 20
|
||||
#' calls to \code{set()}, or if at least 5 seconds have elapsed since the last
|
||||
#' prune occurred, whichever is first. These parameters are currently not
|
||||
#' customizable, but may be in the future.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' \code{max_age}, they will be removed.
|
||||
#'
|
||||
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
#' a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the \code{evict} parameter. Objects will be removed so that the
|
||||
#' number of items is \code{max_n}.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under \code{max_size}. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files -- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' \code{get()} is called. If the target object is older than \code{max_age},
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{"lru"}}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's mtime property. When "lru" is used, each
|
||||
#' \code{get()} is called, it will update the file's mtime.
|
||||
#' }
|
||||
#' \item{\code{"fifo"}}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
#' FAT) have poor mtime resolution. (atime is not used because support for
|
||||
#' atime is worse than mtime.)
|
||||
#'
|
||||
#'
|
||||
#' @section Sharing among multiple processes:
|
||||
#'
|
||||
#' The directory for a DiskCache can be shared among multiple R processes. To
|
||||
#' do this, each R process should have a DiskCache object that uses the same
|
||||
#' directory. Each DiskCache will do pruning independently of the others, so if
|
||||
#' they have different pruning parameters, then one DiskCache may remove cached
|
||||
#' objects before another DiskCache would do so.
|
||||
#'
|
||||
#' Even though it is possible for multiple processes to share a DiskCache
|
||||
#' directory, this should not be done on networked file systems, because of
|
||||
#' slow performance of networked file systems can cause problems. If you need
|
||||
#' a high-performance shared cache, you can use one built on a database like
|
||||
#' Redis, SQLite, mySQL, or similar.
|
||||
#'
|
||||
#' When multiple processes share a cache directory, there are some potential
|
||||
#' race conditions. For example, if your code calls \code{exists(key)} to check
|
||||
#' if an object is in the cache, and then call \code{get(key)}, the object may
|
||||
#' be removed from the cache in between those two calls, and \code{get(key)}
|
||||
#' will throw an error. Instead of calling the two functions, it is better to
|
||||
#' simply call \code{get(key)}, and use \code{tryCatch()} to handle the error
|
||||
#' that is thrown if the object is not in the cache. This effectively tests for
|
||||
#' existence and gets the object in one operation.
|
||||
#'
|
||||
#' It is also possible for one processes to prune objects at the same time that
|
||||
#' another processes is trying to prune objects. If this happens, you may see
|
||||
#' a warning from \code{file.remove()} failing to remove a file that has
|
||||
#' already been deleted.
|
||||
#'
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{get(key, missing, exec_missing)}}{
|
||||
#' Returns the value associated with \code{key}. If the key is not in the
|
||||
#' cache, then it returns the value specified by \code{missing} or,
|
||||
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
#' executes \code{missing}. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{\code{set(key, value)}}{
|
||||
#' Stores the \code{key}-\code{value} pair in the cache.
|
||||
#' }
|
||||
#' \item{\code{exists(key)}}{
|
||||
#' Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
#' \code{FALSE}.
|
||||
#' }
|
||||
#' \item{\code{size()}}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{keys()}}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{reset()}}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{\code{destroy()}}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{\code{prune()}}{
|
||||
#' Prunes the cache, using the parameters specified by \code{max_size},
|
||||
#' \code{max_age}, \code{max_n}, and \code{evict}.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @param dir Directory to store files for the cache. If \code{NULL} (the
|
||||
#' default) it will create and use a temporary directory.
|
||||
#' @param max_age Maximum age of files in cache before they are evicted, in
|
||||
#' seconds. Use \code{Inf} for no age limit.
|
||||
#' @param max_size Maximum size of the cache, in bytes. If the cache exceeds
|
||||
#' this size, cached objects will be removed according to the value of the
|
||||
#' \code{evict}. Use \code{Inf} for no size limit.
|
||||
#' @param max_n Maximum number of objects in the cache. If the number of objects
|
||||
#' exceeds this value, then cached objects will be removed according to the
|
||||
#' value of \code{evict}. Use \code{Inf} for no limit of number of items.
|
||||
#' @param evict The eviction policy to use to decide which objects are removed
|
||||
#' when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
#' supported.
|
||||
#' @param destroy_on_finalize If \code{TRUE}, then when the DiskCache object is
|
||||
#' garbage collected, the cache directory and all objects inside of it will be
|
||||
#' deleted from disk. If \code{FALSE} (the default), it will do nothing when
|
||||
#' finalized.
|
||||
#' @param missing A value to return or a function to execute when
|
||||
#' \code{get(key)} is called but the key is not present in the cache. The
|
||||
#' default is a \code{\link{key_missing}} object. If it is a function to
|
||||
#' execute, the function must take one argument (the key), and you must also
|
||||
#' use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
#' cases for it to throw an error, although another option is to return a
|
||||
#' value. If a value is returned, that value will in turn be returned by
|
||||
#' \code{get()}. See section Missing keys for more information.
|
||||
#' @param exec_missing If \code{FALSE} (the default), then treat \code{missing}
|
||||
#' as a value to return when \code{get()} results in a cache miss. If
|
||||
#' \code{TRUE}, treat \code{missing} as a function to execute when
|
||||
#' \code{get()} results in a cache miss.
|
||||
#' @param logfile An optional filename or connection object to where logging
|
||||
#' information will be written. To log to the console, use \code{stdout()}.
|
||||
#'
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
|
||||
missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
|
||||
DiskCache <- R6Class("DiskCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (is.null(dir)) {
|
||||
dir <- tempfile("DiskCache-")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
|
||||
if (!dirExists(dir)) {
|
||||
private$log(paste0("initialize: Creating ", dir))
|
||||
dir.create(dir, recursive = TRUE)
|
||||
}
|
||||
|
||||
private$dir <- normalizePath(dir)
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$destroy_on_finalize <- destroy_on_finalize
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
|
||||
private$prune_last_time <- as.numeric(Sys.time())
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
filename <- private$key_to_filename(key)
|
||||
|
||||
# Instead of calling exists() before fetching the value, just try to
|
||||
# fetch the value. This reduces the risk of a race condition when
|
||||
# multiple processes share a cache.
|
||||
read_error <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
value <- suppressWarnings(readRDS(filename))
|
||||
if (private$evict == "lru"){
|
||||
Sys.setFileTime(filename, Sys.time())
|
||||
}
|
||||
},
|
||||
error = function(e) {
|
||||
read_error <<- TRUE
|
||||
}
|
||||
)
|
||||
if (read_error) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
file <- private$key_to_filename(key)
|
||||
temp_file <- paste0(file, "-temp-", createUniqueId(8))
|
||||
|
||||
save_error <- FALSE
|
||||
ref_object <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
saveRDS(value, file = temp_file,
|
||||
refhook = function(x) {
|
||||
ref_object <<- TRUE
|
||||
NULL
|
||||
}
|
||||
)
|
||||
file.rename(temp_file, file)
|
||||
},
|
||||
error = function(e) {
|
||||
save_error <<- TRUE
|
||||
# Unlike file.remove(), unlink() does not raise warning if file does
|
||||
# not exist.
|
||||
unlink(temp_file)
|
||||
}
|
||||
)
|
||||
if (save_error) {
|
||||
private$log(paste0('set: key "', key, '" error'))
|
||||
stop('Error setting value for key "', key, '".')
|
||||
}
|
||||
if (ref_object) {
|
||||
private$log(paste0('set: value is a reference object'))
|
||||
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
|
||||
}
|
||||
|
||||
private$prune_throttled()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.exists(private$key_to_filename(key))
|
||||
},
|
||||
|
||||
# Return all keys in the cache
|
||||
keys = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
files <- dir(private$dir, "\\.rds$")
|
||||
sub("\\.rds$", "", files)
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.remove(private$key_to_filename(key))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
# TODO: It would be good to add parameters `n` and `size`, so that the
|
||||
# cache can be pruned to `max_n - n` and `max_size - size` before adding
|
||||
# an object. Right now we prune after adding the object, so the cache
|
||||
# can temporarily grow past the limits. The reason we don't do this now
|
||||
# is because it is expensive to find the size of the serialized object
|
||||
# before adding it.
|
||||
|
||||
private$log(paste0('prune'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
|
||||
current_time <- Sys.time()
|
||||
|
||||
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
|
||||
info <- file.info(filenames)
|
||||
info <- info[info$isdir == FALSE, ]
|
||||
info$name <- rownames(info)
|
||||
rownames(info) <- NULL
|
||||
# Files could be removed between the dir() and file.info() calls. The
|
||||
# entire row for such files will have NA values. Remove those rows.
|
||||
info <- info[!is.na(info$size), ]
|
||||
|
||||
# 1. Remove any files where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
timediff <- as.numeric(current_time - info$mtime, units = "secs")
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority. The sorting is done in a function which can be
|
||||
# called multiple times but only does the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove files if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
# 3. Remove files if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
info <- info[!rm_success, ]
|
||||
}
|
||||
|
||||
private$prune_last_time <- as.numeric(current_time)
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
length(dir(private$dir, "\\.rds$"))
|
||||
},
|
||||
|
||||
destroy = function() {
|
||||
if (self$is_destroyed()) {
|
||||
return(invisible(self))
|
||||
}
|
||||
|
||||
private$log(paste0("destroy: Removing ", private$dir))
|
||||
# First create a sentinel file so that other processes sharing this
|
||||
# cache know that the cache is to be destroyed. This is needed because
|
||||
# the recursive unlink is not atomic: another process can add a file to
|
||||
# the directory after unlink starts removing files but before it removes
|
||||
# the directory, and when that happens, the directory removal will fail.
|
||||
file.create(file.path(private$dir, "__destroyed__"))
|
||||
# Remove all the .rds files. This will not remove the setinel file.
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
# Next remove dir recursively, including sentinel file.
|
||||
unlink(private$dir, recursive = TRUE)
|
||||
private$destroyed <- TRUE
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
is_destroyed = function(throw = FALSE) {
|
||||
if (!dirExists(private$dir) ||
|
||||
file.exists(file.path(private$dir, "__destroyed__")))
|
||||
{
|
||||
# It's possible for another process to destroy a shared cache directory
|
||||
private$destroyed <- TRUE
|
||||
}
|
||||
|
||||
if (throw) {
|
||||
if (private$destroyed) {
|
||||
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
|
||||
}
|
||||
|
||||
} else {
|
||||
private$destroyed
|
||||
}
|
||||
},
|
||||
|
||||
finalize = function() {
|
||||
if (private$destroy_on_finalize) {
|
||||
self$destroy()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
dir = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
destroy_on_finalize = NULL,
|
||||
destroyed = FALSE,
|
||||
missing = NULL,
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL,
|
||||
|
||||
prune_throttle_counter = 0,
|
||||
prune_last_time = NULL,
|
||||
|
||||
key_to_filename = function(key) {
|
||||
validate_key(key)
|
||||
# Additional validation. This 80-char limit is arbitrary, and is
|
||||
# intended to avoid hitting a filename length limit on Windows.
|
||||
if (nchar(key) > 80) {
|
||||
stop("Invalid key: key must have fewer than 80 characters.")
|
||||
}
|
||||
file.path(private$dir, paste0(key, ".rds"))
|
||||
},
|
||||
|
||||
# A wrapper for prune() that throttles it, because prune() can be
|
||||
# expensive due to filesystem operations. This function will prune only
|
||||
# once every 20 times it is called, or if it has been more than 5 seconds
|
||||
# since the last time the cache was actually pruned, whichever is first.
|
||||
# In the future, the behavior may be customizable.
|
||||
prune_throttled = function() {
|
||||
# Count the number of times prune() has been called.
|
||||
private$prune_throttle_counter <- private$prune_throttle_counter + 1
|
||||
|
||||
if (private$prune_throttle_counter > 20 ||
|
||||
private$prune_last_time - as.numeric(Sys.time()) > 5)
|
||||
{
|
||||
self$prune()
|
||||
private$prune_throttle_counter <- 0
|
||||
}
|
||||
},
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
366
R/cache-memory.R
Normal file
366
R/cache-memory.R
Normal file
@@ -0,0 +1,366 @@
|
||||
#' Create a memory cache object
|
||||
#'
|
||||
#' A memory cache object is a key-value store that saves the values in an
|
||||
#' environment. Objects can be stored and retrieved using the \code{get()} and
|
||||
#' \code{set()} methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
#' and \code{evict}.
|
||||
#'
|
||||
#' In a \code{MemoryCache}, R objects are stored directly in the cache; they are
|
||||
#' not \emph{not} serialized before being stored in the cache. This contrasts
|
||||
#' with other cache types, like \code{\link{diskCache}}, where objects are
|
||||
#' serialized, and the serialized object is cached. This can result in some
|
||||
#' differences of behavior. For example, as long as an object is stored in a
|
||||
#' MemoryCache, it will not be garbage collected.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing keys:
|
||||
#' The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
#' when \code{get()} is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
#' object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for \code{missing} that takes one argument, the key, and also use
|
||||
#' \code{exec_missing=TRUE}.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for \code{missing}, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
#' argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that \code{get()} returns a sentinel value
|
||||
#' to represent a cache miss, then \code{set} will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time \code{get()} encounters missing
|
||||
#' key. If the function returns a value, then \code{get()} will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then \code{get()} will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to \code{missing}, and use
|
||||
#' \code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls \code{get()} should be wrapped with
|
||||
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
#' manually by calling \code{prune()}.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' \code{max_age}, they will be removed.
|
||||
#'
|
||||
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
#' a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the \code{evict} parameter. Objects will be removed so that the
|
||||
#' number of items is \code{max_n}.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under \code{max_size}. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files -- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' \code{get()} is called. If the target object is older than \code{max_age},
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{"lru"}}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's atime property. Some filesystems do not
|
||||
#' support atime, or have a very low atime resolution. The DiskCache will
|
||||
#' check for atime support, and if the filesystem does not support atime,
|
||||
#' a warning will be issued and the "fifo" policy will be used instead.
|
||||
#' }
|
||||
#' \item{\code{"fifo"}}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{get(key, missing, exec_missing)}}{
|
||||
#' Returns the value associated with \code{key}. If the key is not in the
|
||||
#' cache, then it returns the value specified by \code{missing} or,
|
||||
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
#' executes \code{missing}. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{\code{set(key, value)}}{
|
||||
#' Stores the \code{key}-\code{value} pair in the cache.
|
||||
#' }
|
||||
#' \item{\code{exists(key)}}{
|
||||
#' Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
#' \code{FALSE}.
|
||||
#' }
|
||||
#' \item{\code{size()}}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{keys()}}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{\code{reset()}}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{\code{destroy()}}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{\code{prune()}}{
|
||||
#' Prunes the cache, using the parameters specified by \code{max_size},
|
||||
#' \code{max_age}, \code{max_n}, and \code{evict}.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams diskCache
|
||||
#'
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
MemoryCache <- R6Class("MemoryCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
private$cache <- new.env(parent = emptyenv())
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
if (!self$exists(key)) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value <- private$cache[[key]]$value
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
time <- as.numeric(Sys.time())
|
||||
|
||||
# Only record size if we're actually using max_size for pruning.
|
||||
if (is.finite(private$max_size)) {
|
||||
# Reported size is rough! See ?object.size.
|
||||
size <- as.numeric(object.size(value))
|
||||
} else {
|
||||
size <- NULL
|
||||
}
|
||||
|
||||
private$cache[[key]] <- list(
|
||||
key = key,
|
||||
value = value,
|
||||
size = size,
|
||||
mtime = time,
|
||||
atime = time
|
||||
)
|
||||
self$prune()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
validate_key(key)
|
||||
# Faster than `exists(key, envir = private$cache, inherits = FALSE)
|
||||
!is.null(private$cache[[key]])
|
||||
},
|
||||
|
||||
keys = function() {
|
||||
ls(private$cache, sorted = FALSE) # Faster with sorted=FALSE
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
validate_key(key)
|
||||
rm(list = key, envir = private$cache)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
rm(list = self$keys(), envir = private$cache)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
private$log(paste0('prune'))
|
||||
info <- private$object_info()
|
||||
|
||||
# 1. Remove any objects where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
time <- as.numeric(Sys.time())
|
||||
timediff <- time - info$mtime
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority, according to eviction policy. The sorting is
|
||||
# done in a function which can be called multiple times but only does
|
||||
# the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
if (private$evict == "lru") {
|
||||
info <<- info[order(info$atime, decreasing = TRUE), ]
|
||||
} else if (private$evict == "fifo") {
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
} else {
|
||||
stop('Unknown eviction policy "', private$evict, '"')
|
||||
}
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove objects if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 3. Remove objects if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
rm(list = info$key[rm_idx], envir = private$cache)
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
length(self$keys())
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
cache = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
missing = NULL,
|
||||
exec_missing = NULL,
|
||||
logfile = NULL,
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
if (!is.finite(private$max_age)) return()
|
||||
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
object_info = function() {
|
||||
keys <- ls(private$cache, sorted = FALSE)
|
||||
data.frame(
|
||||
key = keys,
|
||||
size = vapply(keys, function(key) private$cache[[key]]$size, 0),
|
||||
mtime = vapply(keys, function(key) private$cache[[key]]$mtime, 0),
|
||||
atime = vapply(keys, function(key) private$cache[[key]]$atime, 0),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
|
||||
writeLines(text, private$logfile)
|
||||
}
|
||||
)
|
||||
)
|
||||
33
R/cache-utils.R
Normal file
33
R/cache-utils.R
Normal file
@@ -0,0 +1,33 @@
|
||||
#' A Key Missing object
|
||||
#'
|
||||
#' A \code{key_missing} object represents a cache miss.
|
||||
#'
|
||||
#' @param x An object to test.
|
||||
#'
|
||||
#' @seealso \code{\link{diskCache}}, \code{\link{memoryCache}}.
|
||||
#'
|
||||
#' @export
|
||||
key_missing <- function() {
|
||||
structure(list(), class = "key_missing")
|
||||
}
|
||||
|
||||
#' @rdname key_missing
|
||||
#' @export
|
||||
is.key_missing <- function(x) {
|
||||
inherits(x, "key_missing")
|
||||
}
|
||||
|
||||
#' @export
|
||||
print.key_missing <- function(x, ...) {
|
||||
cat("<Key Missing>\n")
|
||||
}
|
||||
|
||||
|
||||
validate_key <- function(key) {
|
||||
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
|
||||
stop("Invalid key: key must be single non-empty string.")
|
||||
}
|
||||
if (grepl("[^a-z0-9]", key)) {
|
||||
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
|
||||
}
|
||||
}
|
||||
361
R/conditions.R
361
R/conditions.R
@@ -89,6 +89,23 @@ getLocs <- function(calls) {
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
getCallCategories <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile)) {
|
||||
if (!is.null(srcfile$original)) {
|
||||
return("pkg")
|
||||
} else {
|
||||
return("user")
|
||||
}
|
||||
}
|
||||
}
|
||||
return("")
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
#' @details \code{captureStackTraces} runs the given \code{expr} and if any
|
||||
#' \emph{uncaught} errors occur, annotates them with stack trace info for use
|
||||
#' by \code{printError} and \code{printStackTrace}. It is not necessary to use
|
||||
@@ -105,17 +122,93 @@ getLocs <- function(calls) {
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
captureStackTraces <- function(expr) {
|
||||
withCallingHandlers(expr,
|
||||
error = function(e) {
|
||||
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
|
||||
calls <- sys.calls()
|
||||
attr(e, "stack.trace") <- calls
|
||||
stop(e)
|
||||
}
|
||||
}
|
||||
promises::with_promise_domain(createStackTracePromiseDomain(),
|
||||
expr
|
||||
)
|
||||
}
|
||||
|
||||
#' @include globals.R
|
||||
.globals$deepStack <- NULL
|
||||
|
||||
createStackTracePromiseDomain <- function() {
|
||||
# These are actually stateless, we wouldn't have to create a new one each time
|
||||
# if we didn't want to. They're pretty cheap though.
|
||||
|
||||
d <- promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onFulfilled(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onRejected(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
withCallingHandlers(expr,
|
||||
error = doCaptureStack
|
||||
)
|
||||
},
|
||||
onError = doCaptureStack
|
||||
)
|
||||
}
|
||||
|
||||
deepStacksEnabled <- function() {
|
||||
getOption("shiny.deepstacktrace", TRUE)
|
||||
}
|
||||
|
||||
doCaptureStack <- function(e) {
|
||||
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
|
||||
calls <- sys.calls()
|
||||
parents <- sys.parents()
|
||||
attr(calls, "parents") <- parents
|
||||
attr(e, "stack.trace") <- calls
|
||||
}
|
||||
if (deepStacksEnabled()) {
|
||||
if (is.null(attr(e, "deep.stack.trace", exact = TRUE)) && !is.null(.globals$deepStack)) {
|
||||
attr(e, "deep.stack.trace") <- .globals$deepStack
|
||||
}
|
||||
}
|
||||
stop(e)
|
||||
}
|
||||
|
||||
#' @details \code{withLogErrors} captures stack traces and logs errors that
|
||||
#' occur in \code{expr}, but does allow errors to propagate beyond this point
|
||||
#' (i.e. it doesn't catch the error). The same caveats that apply to
|
||||
@@ -128,7 +221,22 @@ withLogErrors <- function(expr,
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
withCallingHandlers(
|
||||
captureStackTraces(expr),
|
||||
{
|
||||
result <- captureStackTraces(expr)
|
||||
|
||||
# Handle expr being an async operation
|
||||
if (promises::is.promise(result)) {
|
||||
result <- promises::catch(result, function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (inherits(cond, "shiny.silent.error")) return()
|
||||
if (isTRUE(getOption("show.error.messages"))) {
|
||||
printError(cond, full = full, offset = offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
error = function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (inherits(cond, "shiny.silent.error")) return()
|
||||
@@ -158,11 +266,11 @@ withLogErrors <- function(expr,
|
||||
printError <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
|
||||
|
||||
printStackTrace(cond, full = full, offset = offset)
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' @rdname stacktrace
|
||||
@@ -171,24 +279,85 @@ printStackTrace <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
stackTrace <- attr(cond, "stack.trace", exact = TRUE)
|
||||
tryCatch(
|
||||
if (!is.null(stackTrace)) {
|
||||
message(paste0(
|
||||
"Stack trace (innermost first):\n",
|
||||
paste0(collapse = "\n",
|
||||
formatStackTrace(stackTrace, full = full, offset = offset,
|
||||
indent = " ")
|
||||
)
|
||||
))
|
||||
} else {
|
||||
message("No stack trace available")
|
||||
},
|
||||
|
||||
error = function(cond) {
|
||||
warning("Failed to write stack trace: ", cond)
|
||||
}
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
stackTraceCalls <- c(
|
||||
attr(cond, "deep.stack.trace", exact = TRUE),
|
||||
list(attr(cond, "stack.trace", exact = TRUE))
|
||||
)
|
||||
|
||||
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
|
||||
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
|
||||
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
|
||||
|
||||
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
|
||||
if (should_drop) {
|
||||
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
|
||||
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
|
||||
# We apply the list of logical vector indices to each data structure
|
||||
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
}
|
||||
|
||||
delayedAssign("all_true", {
|
||||
# List of logical vectors that are all TRUE, the same shape as
|
||||
# stackTraceCallNames. Delay the evaluation so we don't create it unless
|
||||
# we need it, but if we need it twice then we don't pay to create it twice.
|
||||
lapply(stackTraceCallNames, function(st) {
|
||||
rep_len(TRUE, length(st))
|
||||
})
|
||||
})
|
||||
|
||||
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
|
||||
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
|
||||
# logical vectors.
|
||||
toShow <- mapply(
|
||||
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
|
||||
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
|
||||
FUN = `&`,
|
||||
SIMPLIFY = FALSE
|
||||
)
|
||||
|
||||
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
|
||||
st <- data.frame(
|
||||
num = rev(which(index)),
|
||||
call = rev(nms[index]),
|
||||
loc = rev(getLocs(calls[index])),
|
||||
category = rev(getCallCategories(calls[index])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
|
||||
if (nrow(st) == 0) {
|
||||
message(" [No stack trace available]")
|
||||
} else {
|
||||
width <- floor(log10(max(st$num))) + 1
|
||||
formatted <- paste0(
|
||||
" ",
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
cat(file = stderr(), formatted, sep = "")
|
||||
}
|
||||
|
||||
st
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
@@ -196,12 +365,17 @@ printStackTrace <- function(cond,
|
||||
#' from \code{conditionStackTrace(cond)}) and returns a data frame with one
|
||||
#' row for each stack frame and the columns \code{num} (stack frame number),
|
||||
#' \code{call} (a function name or similar), and \code{loc} (source file path
|
||||
#' and line number, if available).
|
||||
#' and line number, if available). It was deprecated after shiny 1.0.5 because
|
||||
#' it doesn't support deep stack traces.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
extractStackTrace <- function(calls,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
if (offset) {
|
||||
@@ -241,7 +415,11 @@ extractStackTrace <- function(calls,
|
||||
score <- rep.int(0, length(callnames))
|
||||
score[callnames == "..stacktraceoff.."] <- -1
|
||||
score[callnames == "..stacktraceon.."] <- 1
|
||||
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff.."))
|
||||
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
|
||||
# just internals for tryCatch
|
||||
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
|
||||
}
|
||||
calls <- calls[toShow]
|
||||
|
||||
@@ -253,12 +431,115 @@ extractStackTrace <- function(calls,
|
||||
num = index,
|
||||
call = getCallNames(calls),
|
||||
loc = getLocs(calls),
|
||||
category = getCallCategories(calls),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
stripStackTraces <- function(stackTraces, values = FALSE) {
|
||||
score <- 1L # >=1: show, <=0: hide
|
||||
lapply(seq_along(stackTraces), function(i) {
|
||||
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
|
||||
score <<- res$score
|
||||
toShow <- as.logical(res$trace)
|
||||
if (values) {
|
||||
as.character(stackTraces[[i]][toShow])
|
||||
} else {
|
||||
as.logical(toShow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
|
||||
prefix <- logical(0)
|
||||
if (truncateFloor) {
|
||||
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
|
||||
if (length(indexOfFloor)) {
|
||||
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
|
||||
prefix <- rep_len(FALSE, indexOfFloor)
|
||||
}
|
||||
}
|
||||
|
||||
if (length(stackTrace) == 0) {
|
||||
return(list(score = startingScore, character(0)))
|
||||
}
|
||||
|
||||
score <- rep.int(0L, length(stackTrace))
|
||||
score[stackTrace == "..stacktraceon.."] <- 1L
|
||||
score[stackTrace == "..stacktraceoff.."] <- -1L
|
||||
score <- startingScore + cumsum(score)
|
||||
|
||||
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
|
||||
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
|
||||
}
|
||||
|
||||
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
|
||||
# that prunes each subtree so that only the final branch remains. The result,
|
||||
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
|
||||
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
|
||||
# part of the active call stack, they rarely are helpful when trying to identify
|
||||
# a broken bit of code.
|
||||
pruneStackTrace <- function(parents) {
|
||||
# Detect nodes that are not the last child. This is necessary, but not
|
||||
# sufficient; we also need to drop nodes that are the last child, but one of
|
||||
# their ancestors is not.
|
||||
is_dupe <- duplicated(parents, fromLast = TRUE)
|
||||
|
||||
# The index of the most recently seen node that was actually kept instead of
|
||||
# dropped.
|
||||
current_node <- 0
|
||||
|
||||
# Loop over the parent indices. Anything that is not parented by current_node
|
||||
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
|
||||
# is kept becomes the new current_node.
|
||||
include <- vapply(seq_along(parents), function(i) {
|
||||
if (!is_dupe[[i]] && parents[[i]] == current_node) {
|
||||
current_node <<- i
|
||||
TRUE
|
||||
} else {
|
||||
FALSE
|
||||
}
|
||||
}, FUN.VALUE = logical(1))
|
||||
|
||||
include
|
||||
}
|
||||
|
||||
dropTrivialFrames <- function(callnames) {
|
||||
# Remove stop(), .handleSimpleError(), and h() calls from the end of
|
||||
# the calls--they don't add any helpful information. But only remove
|
||||
# the last *contiguous* block of them, and then, only if they are the
|
||||
# last thing in the calls list.
|
||||
hideable <- callnames %in% c(".handleSimpleError", "h", "base$wrapOnFulfilled")
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(callnames) - lastGoodCall
|
||||
|
||||
c(
|
||||
rep_len(TRUE, length(callnames) - toRemove),
|
||||
rep_len(FALSE, toRemove)
|
||||
)
|
||||
}
|
||||
|
||||
offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
if (offset) {
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
|
||||
# Offset calls vs. srcrefs by 1 to make them more intuitive.
|
||||
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
|
||||
# the definition of foo().
|
||||
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
|
||||
|
||||
calls <- setSrcRefs(calls, srcrefs)
|
||||
}
|
||||
|
||||
calls
|
||||
}
|
||||
|
||||
#' @details \code{formatStackTrace} is similar to \code{extractStackTrace}, but
|
||||
#' it returns a preformatted character vector instead of a data frame.
|
||||
#' it returns a preformatted character vector instead of a data frame. It was
|
||||
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
|
||||
#' @param indent A string to prefix every line of the stack trace.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
@@ -266,6 +547,10 @@ formatStackTrace <- function(calls, indent = " ",
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
st <- extractStackTrace(calls, full = full, offset = offset)
|
||||
if (nrow(st) == 0) {
|
||||
return(character(0))
|
||||
@@ -276,8 +561,14 @@ formatStackTrace <- function(calls, indent = " ",
|
||||
indent,
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
st$call,
|
||||
st$loc
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -332,3 +623,5 @@ conditionStackTrace <- function(cond) {
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
..stacktraceoff.. <- function(expr) expr
|
||||
|
||||
..stacktracefloor.. <- function(expr) expr
|
||||
@@ -1,3 +1,33 @@
|
||||
startPNG <- function(filename, width, height, res, ...) {
|
||||
# If quartz is available, use png() (which will default to quartz).
|
||||
# Otherwise, if the Cairo package is installed, use CairoPNG().
|
||||
# Finally, if neither quartz nor Cairo, use png().
|
||||
if (capabilities("aqua")) {
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
|
||||
nchar(system.file(package = "Cairo"))) {
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
pngfun <- grDevices::png
|
||||
}
|
||||
|
||||
pngfun(filename=filename, width=width, height=height, res=res, ...)
|
||||
# Call plot.new() so that even if no plotting operations are performed at
|
||||
# least we have a blank background. N.B. we need to set the margin to 0
|
||||
# temporarily before plot.new() because when the plot size is small (e.g.
|
||||
# 200x50), we will get an error "figure margin too large", which is triggered
|
||||
# by plot.new() with the default (large) margin. However, this does not
|
||||
# guarantee user's code in func() will not trigger the error -- they may have
|
||||
# to set par(mar = smaller_value) before they draw base graphics.
|
||||
op <- graphics::par(mar = rep(0, 4))
|
||||
tryCatch(
|
||||
graphics::plot.new(),
|
||||
finally = graphics::par(op)
|
||||
)
|
||||
|
||||
grDevices::dev.cur()
|
||||
}
|
||||
|
||||
#' Run a plotting function and save the output as a PNG
|
||||
#'
|
||||
#' This function returns the name of the PNG file that it generates. In
|
||||
@@ -28,35 +58,44 @@
|
||||
#' @export
|
||||
plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
width=400, height=400, res=72, ...) {
|
||||
# If quartz is available, use png() (which will default to quartz).
|
||||
# Otherwise, if the Cairo package is installed, use CairoPNG().
|
||||
# Finally, if neither quartz nor Cairo, use png().
|
||||
if (capabilities("aqua")) {
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
|
||||
nchar(system.file(package = "Cairo"))) {
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
pngfun <- grDevices::png
|
||||
}
|
||||
|
||||
pngfun(filename=filename, width=width, height=height, res=res, ...)
|
||||
# Call plot.new() so that even if no plotting operations are performed at
|
||||
# least we have a blank background. N.B. we need to set the margin to 0
|
||||
# temporarily before plot.new() because when the plot size is small (e.g.
|
||||
# 200x50), we will get an error "figure margin too large", which is triggered
|
||||
# by plot.new() with the default (large) margin. However, this does not
|
||||
# guarantee user's code in func() will not trigger the error -- they may have
|
||||
# to set par(mar = smaller_value) before they draw base graphics.
|
||||
op <- graphics::par(mar = rep(0, 4))
|
||||
tryCatch(
|
||||
graphics::plot.new(),
|
||||
finally = graphics::par(op)
|
||||
)
|
||||
|
||||
dv <- grDevices::dev.cur()
|
||||
dv <- startPNG(filename, width, height, res, ...)
|
||||
on.exit(grDevices::dev.off(dv), add = TRUE)
|
||||
func()
|
||||
|
||||
filename
|
||||
}
|
||||
|
||||
#' @importFrom grDevices dev.set dev.cur
|
||||
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
|
||||
force(which)
|
||||
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
force(expr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
#' "nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
|
||||
#' "ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
|
||||
#' "vi", "zh-CN", and "zh-TW".
|
||||
#' @param autoclose Whether or not to close the datepicker immediately when a
|
||||
#' date is selected.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
|
||||
@@ -76,7 +78,7 @@
|
||||
#' @export
|
||||
dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
format = "yyyy-mm-dd", startview = "month", weekstart = 0, language = "en",
|
||||
width = NULL) {
|
||||
width = NULL, autoclose = TRUE) {
|
||||
|
||||
# If value is a date object, convert it to a string with yyyy-mm-dd format
|
||||
# Same for min and max
|
||||
@@ -99,7 +101,8 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = value
|
||||
`data-initial-date` = value,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
),
|
||||
datePickerDependency
|
||||
)
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
#' @export
|
||||
dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
min = NULL, max = NULL, format = "yyyy-mm-dd", startview = "month",
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL) {
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL,
|
||||
autoclose = TRUE) {
|
||||
|
||||
# If start and end are date objects, convert to a string with yyyy-mm-dd format
|
||||
# Same for min and max
|
||||
@@ -103,7 +104,8 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = start
|
||||
`data-initial-date` = start,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
),
|
||||
span(class = "input-group-addon", separator),
|
||||
tags$input(
|
||||
@@ -115,7 +117,8 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = end
|
||||
`data-initial-date` = end,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
140
R/input-select.R
140
R/input-select.R
@@ -33,7 +33,7 @@
|
||||
#' @return A select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateSelectInput}}
|
||||
#' @seealso \code{\link{updateSelectInput}} \code{\link{varSelectInput}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
@@ -59,9 +59,9 @@
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' selectInput("state", "Choose a state:",
|
||||
#' list(`East Coast` = c("NY", "NJ", "CT"),
|
||||
#' `West Coast` = c("WA", "OR", "CA"),
|
||||
#' `Midwest` = c("MN", "WI", "IA"))
|
||||
#' list(`East Coast` = list("NY", "NJ", "CT"),
|
||||
#' `West Coast` = list("WA", "OR", "CA"),
|
||||
#' `Midwest` = list("MN", "WI", "IA"))
|
||||
#' ),
|
||||
#' textOutput("result")
|
||||
#' ),
|
||||
@@ -212,3 +212,135 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
|
||||
attachDependencies(select, selectizeDep)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#' Select variables from a data frame
|
||||
#'
|
||||
#' Create a select list that can be used to choose a single or multiple items
|
||||
#' from the column names of a data frame.
|
||||
#'
|
||||
#' The resulting server \code{input} value will be returned as:
|
||||
#' \itemize{
|
||||
#' \item a symbol if \code{multiple = FALSE}. The \code{input} value should be
|
||||
#' used with rlang's \code{\link[rlang]{!!}}. For example,
|
||||
#' \code{ggplot2::aes(!!input$variable)}.
|
||||
#' \item a list of symbols if \code{multiple = TRUE}. The \code{input} value
|
||||
#' should be used with rlang's \code{\link[rlang]{!!!}} to expand
|
||||
#' the symbol list as individual arguments. For example,
|
||||
#' \code{dplyr::select(mtcars, !!!input$variabls)} which is
|
||||
#' equivalent to \code{dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])}.
|
||||
#' }
|
||||
#'
|
||||
#' By default, \code{varSelectInput()} and \code{selectizeInput()} use the
|
||||
#' JavaScript library \pkg{selectize.js}
|
||||
#' (\url{https://github.com/selectize/selectize.js}) to instead of the basic
|
||||
#' select input element. To use the standard HTML select input element, use
|
||||
#' \code{selectInput()} with \code{selectize=FALSE}.
|
||||
#'
|
||||
#' @inheritParams selectInput
|
||||
#' @param data A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}
|
||||
#' @return A variable select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso \code{\link{updateSelectInput}}
|
||||
#' @examples
|
||||
#'
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' library(ggplot2)
|
||||
#'
|
||||
#' # single selection
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variable", "Variable:", mtcars),
|
||||
#' plotOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderPlot({
|
||||
#' ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # multiple selections
|
||||
#' \dontrun{
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
|
||||
#' tableOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderTable({
|
||||
#' if (length(input$variables) == 0) return(mtcars)
|
||||
#' mtcars %>% dplyr::select(!!!input$variables)
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#' )}
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
varSelectInput <- function(
|
||||
inputId, label, data, selected = NULL,
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL
|
||||
) {
|
||||
# no place holders
|
||||
choices <- colnames(data)
|
||||
|
||||
selectInputVal <- selectInput(
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected,
|
||||
multiple = multiple,
|
||||
selectize = selectize,
|
||||
width = width,
|
||||
size = size
|
||||
)
|
||||
|
||||
# set the select tag class to be "symbol"
|
||||
selectClass <- selectInputVal$children[[2]]$children[[1]]$attribs$class
|
||||
if (is.null(selectClass)) {
|
||||
newClass <- "symbol"
|
||||
} else {
|
||||
newClass <- paste(selectClass, "symbol", sep = " ")
|
||||
}
|
||||
selectInputVal$children[[2]]$children[[1]]$attribs$class <- newClass
|
||||
|
||||
selectInputVal
|
||||
}
|
||||
|
||||
|
||||
|
||||
#' @rdname varSelectInput
|
||||
#' @param ... Arguments passed to \code{varSelectInput()}.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}
|
||||
#' for possible options (character option values inside \code{\link[base]{I}()} will
|
||||
#' be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
|
||||
#' for details).
|
||||
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
#' see \code{\link{validateCssUnit}}.
|
||||
#' @note The variable selectize input created from \code{varSelectizeInput()} allows
|
||||
#' deletion of the selected option even in a single select input, which will
|
||||
#' return an empty string as its value. This is the default behavior of
|
||||
#' \pkg{selectize.js}. However, the selectize input created from
|
||||
#' \code{selectInput(..., selectize = TRUE)} will ignore the empty string
|
||||
#' value when it is a single choice input and the empty string is not in the
|
||||
#' \code{choices} argument. This is to keep compatibility with
|
||||
#' \code{selectInput(..., selectize = FALSE)}.
|
||||
#' @export
|
||||
varSelectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
selectizeIt(
|
||||
inputId,
|
||||
varSelectInput(inputId, ..., selectize = FALSE, width = width),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,40 +86,25 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
version = "0.10.2.2")
|
||||
}
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
|
||||
range <- max - min
|
||||
# If short range or decimals, use continuous decimal with ~100 points
|
||||
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
|
||||
step <- pretty(c(min, max), n = 100)
|
||||
step[2] - step[1]
|
||||
} else {
|
||||
1
|
||||
}
|
||||
if (is.null(timeFormat)) {
|
||||
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
|
||||
}
|
||||
|
||||
if (inherits(min, "Date")) {
|
||||
if (!inherits(max, "Date") || !inherits(value, "Date"))
|
||||
stop("`min`, `max`, and `value must all be Date or non-Date objects")
|
||||
dataType <- "date"
|
||||
# Restore bookmarked values here, after doing the type checking, because the
|
||||
# restored value will be a character vector instead of Date or POSIXct, and we can do
|
||||
# the conversion to correct type next.
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
if (is.null(timeFormat))
|
||||
timeFormat <- "%F"
|
||||
|
||||
} else if (inherits(min, "POSIXt")) {
|
||||
if (!inherits(max, "POSIXt") || !inherits(value, "POSIXt"))
|
||||
stop("`min`, `max`, and `value must all be POSIXt or non-POSIXt objects")
|
||||
dataType <- "datetime"
|
||||
|
||||
if (is.null(timeFormat))
|
||||
timeFormat <- "%F %T"
|
||||
|
||||
} else {
|
||||
dataType <- "number"
|
||||
if (is.character(value)) {
|
||||
# If we got here, the value was restored from a URL-encoded bookmark.
|
||||
if (dataType == "date") {
|
||||
value <- as.Date(value, format = "%Y-%m-%d")
|
||||
} else if (dataType == "datetime") {
|
||||
# Date-times will have a format like "2018-02-28T03:46:26Z"
|
||||
value <- as.POSIXct(value, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")
|
||||
}
|
||||
}
|
||||
|
||||
step <- findStepSize(min, max, step)
|
||||
@@ -169,7 +154,6 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
`data-prefix` = pre,
|
||||
`data-postfix` = post,
|
||||
`data-keyboard` = TRUE,
|
||||
`data-keyboard-step` = step / (max - min) * 100,
|
||||
# This value is only relevant for range sliders; for non-range sliders it
|
||||
# causes problems since ion.RangeSlider 2.1.2 (issue #1605).
|
||||
`data-drag-interval` = if (length(value) > 1) dragRange,
|
||||
@@ -238,6 +222,34 @@ hasDecimals <- function(value) {
|
||||
return (!identical(value, truncatedValue))
|
||||
}
|
||||
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
|
||||
range <- max - min
|
||||
# If short range or decimals, use continuous decimal with ~100 points
|
||||
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
|
||||
# Workaround for rounding errors (#1006): the intervals between the items
|
||||
# returned by pretty() can have rounding errors. To avoid this, we'll use
|
||||
# pretty() to find the min, max, and number of steps, and then use those
|
||||
# values to calculate the step size.
|
||||
pretty_steps <- pretty(c(min, max), n = 100)
|
||||
n_steps <- length(pretty_steps) - 1
|
||||
|
||||
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
|
||||
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
|
||||
# which should be way way less significant than any data we'd want to keep.
|
||||
# It might make sense to use signif(steps[2] - steps[1], 10) instead, but
|
||||
# for now trying to make the minimal change.
|
||||
signif(digits = 10, (max(pretty_steps) - min(pretty_steps)) / n_steps)
|
||||
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' @rdname sliderInput
|
||||
#'
|
||||
#' @param interval The interval, in milliseconds, between each animation step.
|
||||
|
||||
@@ -351,35 +351,38 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
}
|
||||
|
||||
response <- handler(req)
|
||||
if (is.null(response))
|
||||
response <- httpResponse(404, content="<h1>Not Found</h1>")
|
||||
|
||||
if (inherits(response, "httpResponse")) {
|
||||
headers <- as.list(response$headers)
|
||||
headers$'Content-Type' <- response$content_type
|
||||
res <- hybrid_chain(response, function(response) {
|
||||
if (is.null(response))
|
||||
response <- httpResponse(404, content="<h1>Not Found</h1>")
|
||||
|
||||
response <- filter(req, response)
|
||||
if (head_request) {
|
||||
if (inherits(response, "httpResponse")) {
|
||||
headers <- as.list(response$headers)
|
||||
headers$'Content-Type' <- response$content_type
|
||||
|
||||
headers$`Content-Length` <- getResponseContentLength(response, deleteOwnedContent = TRUE)
|
||||
response <- filter(req, response)
|
||||
if (head_request) {
|
||||
|
||||
headers$`Content-Length` <- getResponseContentLength(response, deleteOwnedContent = TRUE)
|
||||
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = "",
|
||||
headers = headers
|
||||
))
|
||||
} else {
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = response$content,
|
||||
headers = headers
|
||||
))
|
||||
}
|
||||
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = "",
|
||||
headers = headers
|
||||
))
|
||||
} else {
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = response$content,
|
||||
headers = headers
|
||||
))
|
||||
# Assume it's a Rook-compatible response
|
||||
return(response)
|
||||
}
|
||||
|
||||
} else {
|
||||
# Assume it's a Rook-compatible response
|
||||
return(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
92
R/react.R
92
R/react.R
@@ -1,3 +1,21 @@
|
||||
processId <- local({
|
||||
# pid is not sufficient to uniquely identify a process, because
|
||||
# distributed futures span machines which could introduce pid
|
||||
# collisions.
|
||||
cached <- NULL
|
||||
function() {
|
||||
if (is.null(cached)) {
|
||||
cached <<- digest::digest(list(
|
||||
Sys.info(),
|
||||
Sys.time()
|
||||
))
|
||||
}
|
||||
# Sys.getpid() cannot be cached because forked children will
|
||||
# then have the same processId as their parents.
|
||||
paste(cached, Sys.getpid())
|
||||
}
|
||||
})
|
||||
|
||||
Context <- R6Class(
|
||||
'Context',
|
||||
portable = FALSE,
|
||||
@@ -9,25 +27,35 @@ Context <- R6Class(
|
||||
.invalidateCallbacks = list(),
|
||||
.flushCallbacks = list(),
|
||||
.domain = NULL,
|
||||
.pid = NULL,
|
||||
|
||||
initialize = function(domain, label='', type='other', prevId='') {
|
||||
id <<- .getReactiveEnvironment()$nextId()
|
||||
.label <<- label
|
||||
.domain <<- domain
|
||||
.pid <<- processId()
|
||||
.graphCreateContext(id, label, type, prevId, domain)
|
||||
},
|
||||
run = function(func) {
|
||||
"Run the provided function under this context."
|
||||
withReactiveDomain(.domain, {
|
||||
env <- .getReactiveEnvironment()
|
||||
.graphEnterContext(id)
|
||||
on.exit(.graphExitContext(id), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
withReactiveDomain(.domain, {
|
||||
env <- .getReactiveEnvironment()
|
||||
.graphEnterContext(id)
|
||||
on.exit(.graphExitContext(id), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
})
|
||||
})
|
||||
},
|
||||
invalidate = function() {
|
||||
"Invalidate this context. It will immediately call the callbacks
|
||||
that have been registered with onInvalidate()."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and invalidated from another")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
return()
|
||||
.invalidated <<- TRUE
|
||||
@@ -43,6 +71,11 @@ Context <- R6Class(
|
||||
"Register a function to be called when this context is invalidated.
|
||||
If this context is already invalidated, the function is called
|
||||
immediately."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and accessed from another")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
func()
|
||||
else
|
||||
@@ -52,9 +85,6 @@ Context <- R6Class(
|
||||
addPendingFlush = function(priority) {
|
||||
"Tell the reactive environment that this context should be flushed the
|
||||
next time flushReact() called."
|
||||
if (!is.null(.domain)) {
|
||||
.domain$incrementBusyCount()
|
||||
}
|
||||
.getReactiveEnvironment()$addPendingFlush(self, priority)
|
||||
},
|
||||
onFlush = function(func) {
|
||||
@@ -64,12 +94,6 @@ Context <- R6Class(
|
||||
executeFlushCallbacks = function() {
|
||||
"For internal use only."
|
||||
|
||||
on.exit({
|
||||
if (!is.null(.domain)) {
|
||||
.domain$decrementBusyCount()
|
||||
}
|
||||
}, add = TRUE)
|
||||
|
||||
lapply(.flushCallbacks, function(flushCallback) {
|
||||
flushCallback()
|
||||
})
|
||||
@@ -118,9 +142,12 @@ ReactiveEnvironment <- R6Class(
|
||||
hasPendingFlush = function() {
|
||||
return(!.pendingFlush$isEmpty())
|
||||
},
|
||||
# Returns TRUE if anything was actually called
|
||||
flush = function() {
|
||||
# If nothing to flush, exit early
|
||||
if (!hasPendingFlush()) return(invisible(FALSE))
|
||||
# If already in a flush, don't start another one
|
||||
if (.inFlush) return()
|
||||
if (.inFlush) return(invisible(FALSE))
|
||||
.inFlush <<- TRUE
|
||||
on.exit(.inFlush <<- FALSE)
|
||||
|
||||
@@ -128,6 +155,8 @@ ReactiveEnvironment <- R6Class(
|
||||
ctx <- .pendingFlush$dequeue()
|
||||
ctx$executeFlushCallbacks()
|
||||
}
|
||||
|
||||
invisible(TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -141,9 +170,10 @@ ReactiveEnvironment <- R6Class(
|
||||
}
|
||||
})
|
||||
|
||||
# Causes any pending invalidations to run.
|
||||
# Causes any pending invalidations to run. Returns TRUE if any invalidations
|
||||
# were pending (i.e. if work was actually done).
|
||||
flushReact <- function() {
|
||||
.getReactiveEnvironment()$flush()
|
||||
return(.getReactiveEnvironment()$flush())
|
||||
}
|
||||
|
||||
# Retrieves the current reactive context, or errors if there is no reactive
|
||||
@@ -163,3 +193,31 @@ local({
|
||||
return(dummyContext)
|
||||
}
|
||||
})
|
||||
|
||||
wrapForContext <- function(func, ctx) {
|
||||
force(func)
|
||||
force(ctx)
|
||||
|
||||
function(...) {
|
||||
ctx$run(function() {
|
||||
captureStackTraces(
|
||||
func(...)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reactivePromiseDomain <- function() {
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
ctx <- getCurrentContext()
|
||||
wrapForContext(onFulfilled, ctx)
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
ctx <- getCurrentContext()
|
||||
wrapForContext(onRejected, ctx)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,11 +95,7 @@ getDefaultReactiveDomain <- function() {
|
||||
#' @rdname domains
|
||||
#' @export
|
||||
withReactiveDomain <- function(domain, expr) {
|
||||
oldValue <- .globals$domain
|
||||
.globals$domain <- domain
|
||||
on.exit(.globals$domain <- oldValue)
|
||||
|
||||
expr
|
||||
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
295
R/reactives.R
295
R/reactives.R
@@ -91,7 +91,7 @@ ReactiveVal <- R6Class(
|
||||
format = function(...) {
|
||||
# capture.output(print()) is necessary because format() doesn't
|
||||
# necessarily return a character vector, e.g. data.frame.
|
||||
label <- capture.output(print(base::format(private$value, ...)))
|
||||
label <- utils::capture.output(print(base::format(private$value, ...)))
|
||||
if (length(label) == 1) {
|
||||
paste0("reactiveVal: ", label)
|
||||
} else {
|
||||
@@ -278,8 +278,9 @@ ReactiveValues <- R6Class(
|
||||
.allValuesDeps = 'Dependents',
|
||||
# Dependents for all values
|
||||
.valuesDeps = 'Dependents',
|
||||
.dedupe = logical(0),
|
||||
|
||||
initialize = function() {
|
||||
initialize = function(dedupe = TRUE) {
|
||||
.label <<- paste('reactiveValues',
|
||||
p_randomInt(1000, 10000),
|
||||
sep="")
|
||||
@@ -289,6 +290,7 @@ ReactiveValues <- R6Class(
|
||||
.namesDeps <<- Dependents$new()
|
||||
.allValuesDeps <<- Dependents$new()
|
||||
.valuesDeps <<- Dependents$new()
|
||||
.dedupe <<- dedupe
|
||||
},
|
||||
|
||||
get = function(key) {
|
||||
@@ -317,7 +319,7 @@ ReactiveValues <- R6Class(
|
||||
hidden <- substr(key, 1, 1) == "."
|
||||
|
||||
if (exists(key, envir=.values, inherits=FALSE)) {
|
||||
if (identical(.values[[key]], value)) {
|
||||
if (.dedupe && identical(.values[[key]], value)) {
|
||||
return(invisible())
|
||||
}
|
||||
}
|
||||
@@ -781,18 +783,6 @@ Observable <- R6Class(
|
||||
# If an error occurs, we want to propagate the error, but we also
|
||||
# want to save a copy of it, so future callers of this reactive will
|
||||
# get the same error (i.e. the error is cached).
|
||||
|
||||
# We stripStackTrace in the next line, just in case someone
|
||||
# downstream of us (i.e. deeper into the call stack) used
|
||||
# captureStackTraces; otherwise the entire stack would always be the
|
||||
# same (i.e. you'd always see the whole stack trace of the *first*
|
||||
# time the code was run and the condition raised; there'd be no way
|
||||
# to see the stack trace of the call site that caused the cached
|
||||
# exception to be re-raised, and you need that information to figure
|
||||
# out what's triggering the re-raise).
|
||||
#
|
||||
# We use try(stop()) as an easy way to generate a try-error object
|
||||
# out of this condition.
|
||||
.value <<- cond
|
||||
.error <<- TRUE
|
||||
.visible <<- FALSE
|
||||
@@ -969,19 +959,12 @@ Observer <- R6Class(
|
||||
if (length(formals(observerFunc)) > 0)
|
||||
stop("Can't make an observer from a function that takes parameters; ",
|
||||
"only functions without parameters can be reactive.")
|
||||
registerDebugHook("observerFunc", environment(), label)
|
||||
.func <<- function() {
|
||||
tryCatch(
|
||||
if (..stacktraceon)
|
||||
..stacktraceon..(observerFunc())
|
||||
else
|
||||
observerFunc(),
|
||||
# It's OK for shiny.silent.error errors to cause an observer to stop running
|
||||
shiny.silent.error = function(e) NULL
|
||||
# validation = function(e) NULL,
|
||||
# shiny.output.cancel = function(e) NULL
|
||||
)
|
||||
if (grepl("\\s", label, perl = TRUE)) {
|
||||
funcLabel <- "<observer>"
|
||||
} else {
|
||||
funcLabel <- paste0("<observer:", label, ">")
|
||||
}
|
||||
.func <<- wrapFunctionLabel(observerFunc, funcLabel, ..stacktraceon = ..stacktraceon)
|
||||
.label <<- label
|
||||
.domain <<- domain
|
||||
.priority <<- normalizePriority(priority)
|
||||
@@ -1026,6 +1009,9 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
|
||||
continue <- function() {
|
||||
ctx$addPendingFlush(.priority)
|
||||
if (!is.null(.domain)) {
|
||||
.domain$incrementBusyCount()
|
||||
}
|
||||
}
|
||||
|
||||
if (.suspended == FALSE)
|
||||
@@ -1035,16 +1021,30 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
})
|
||||
|
||||
ctx$onFlush(function() {
|
||||
tryCatch({
|
||||
if (!.destroyed)
|
||||
shinyCallingHandlers(run())
|
||||
|
||||
}, error = function(e) {
|
||||
printError(e)
|
||||
if (!is.null(.domain)) {
|
||||
.domain$unhandledError(e)
|
||||
}
|
||||
})
|
||||
hybrid_chain(
|
||||
{
|
||||
if (!.destroyed) {
|
||||
shinyCallingHandlers(run())
|
||||
}
|
||||
},
|
||||
catch = function(e) {
|
||||
# It's OK for shiny.silent.error errors to cause an observer to stop running
|
||||
# shiny.silent.error = function(e) NULL
|
||||
# validation = function(e) NULL,
|
||||
# shiny.output.cancel = function(e) NULL
|
||||
|
||||
if (inherits(e, "shiny.silent.error")) {
|
||||
return()
|
||||
}
|
||||
|
||||
printError(e)
|
||||
if (!is.null(.domain)) {
|
||||
.domain$unhandledError(e)
|
||||
}
|
||||
},
|
||||
finally = .domain$decrementBusyCount
|
||||
)
|
||||
})
|
||||
|
||||
return(ctx)
|
||||
@@ -1394,20 +1394,28 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
|
||||
force(session)
|
||||
|
||||
dependents <- Map$new()
|
||||
timerCallbacks$schedule(intervalMs, function() {
|
||||
timerHandle <- scheduleTask(intervalMs, function() {
|
||||
# Quit if the session is closed
|
||||
if (!is.null(session) && session$isClosed()) {
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
timerCallbacks$schedule(intervalMs, sys.function())
|
||||
lapply(
|
||||
dependents$values(),
|
||||
function(dep.ctx) {
|
||||
dep.ctx$invalidate()
|
||||
NULL
|
||||
})
|
||||
timerHandle <<- scheduleTask(intervalMs, sys.function())
|
||||
|
||||
session$cycleStartAction(function() {
|
||||
lapply(
|
||||
dependents$values(),
|
||||
function(dep.ctx) {
|
||||
dep.ctx$invalidate()
|
||||
NULL
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (!is.null(session)) {
|
||||
session$onEnded(timerHandle)
|
||||
}
|
||||
|
||||
return(function() {
|
||||
ctx <- .getReactiveEnvironment()$currentContext()
|
||||
if (!dependents$containsKey(ctx$id)) {
|
||||
@@ -1475,14 +1483,27 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
|
||||
#' }
|
||||
#' @export
|
||||
invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
|
||||
force(session)
|
||||
ctx <- .getReactiveEnvironment()$currentContext()
|
||||
timerCallbacks$schedule(millis, function() {
|
||||
# Quit if the session is closed
|
||||
if (!is.null(session) && session$isClosed()) {
|
||||
timerHandle <- scheduleTask(millis, function() {
|
||||
if (is.null(session)) {
|
||||
ctx$invalidate()
|
||||
return(invisible())
|
||||
}
|
||||
ctx$invalidate()
|
||||
|
||||
if (!session$isClosed()) {
|
||||
session$cycleStartAction(function() {
|
||||
ctx$invalidate()
|
||||
})
|
||||
}
|
||||
|
||||
invisible()
|
||||
})
|
||||
|
||||
if (!is.null(session)) {
|
||||
session$onEnded(timerHandle)
|
||||
}
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
@@ -1800,15 +1821,20 @@ maskReactiveContext <- function(expr) {
|
||||
#' the action/calculation and just let the user re-initiate it (like a
|
||||
#' "Recalculate" button).
|
||||
#'
|
||||
#' Unlike what happens for \code{ignoreNULL}, only \code{observeEvent} takes in an
|
||||
#' \code{ignoreInit} argument. By default, \code{observeEvent} will run right when
|
||||
#' it is created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
|
||||
#' Likewise, both \code{observeEvent} and \code{eventReactive} also take in an
|
||||
#' \code{ignoreInit} argument. By default, both of these will run right when they
|
||||
#' are created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
|
||||
#' and \code{ignoreNULL} is \code{TRUE}). But when responding to a click of an action
|
||||
#' button, it may often be useful to set \code{ignoreInit} to \code{TRUE}. For
|
||||
#' example, if you're setting up an \code{observeEvent} for a dynamically created
|
||||
#' button, then \code{ignoreInit = TRUE} will guarantee that the action (in
|
||||
#' \code{handlerExpr}) will only be triggered when the button is actually clicked,
|
||||
#' instead of also being triggered when it is created/initialized.
|
||||
#' instead of also being triggered when it is created/initialized. Similarly,
|
||||
#' if you're setting up an \code{eventReactive} that responds to a dynamically
|
||||
#' created button used to refresh some data (then returned by that \code{eventReactive}),
|
||||
#' then you should use \code{eventReactive([...], ignoreInit = TRUE)} if you want
|
||||
#' to let the user decide if/when they want to refresh the data (since, depending
|
||||
#' on the app, this may be a computationally expensive operation).
|
||||
#'
|
||||
#' 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
|
||||
@@ -1816,25 +1842,28 @@ maskReactiveContext <- function(expr) {
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
|
||||
#' This is the default. This combination means that \code{handlerExpr} will
|
||||
#' run every time that \code{eventExpr} is not \code{NULL}. If, at the time
|
||||
#' of the \code{observeEvent}'s creation, \code{handleExpr} happens to
|
||||
#' \emph{not} be \code{NULL}, then the code runs.
|
||||
#' This is the default. This combination means that \code{handlerExpr}/
|
||||
#' \code{valueExpr} will run every time that \code{eventExpr} is not
|
||||
#' \code{NULL}. If, at the time of the creation of the
|
||||
#' \code{observeEvent}/\code{eventReactive}, \code{eventExpr} happens
|
||||
#' to \emph{not} be \code{NULL}, then the code runs.
|
||||
#' }
|
||||
#' \item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
|
||||
#' This combination means that \code{handlerExpr} will run every time no
|
||||
#' matter what.
|
||||
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
#' run every time no matter what.
|
||||
#' }
|
||||
#' \item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
|
||||
#' This combination means that \code{handlerExpr} will \emph{not} run when
|
||||
#' the \code{observeEvent} is created (because \code{ignoreInit = TRUE}),
|
||||
#' but it will run every other time.
|
||||
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
#' \emph{not} run when the \code{observeEvent}/\code{eventReactive} is
|
||||
#' created (because \code{ignoreInit = TRUE}), but it will run every
|
||||
#' other time.
|
||||
#' }
|
||||
#' \item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
|
||||
#' This combination means that \code{handlerExpr} will \emph{not} run when
|
||||
#' the \code{observeEvent} is created (because \code{ignoreInit = TRUE}).
|
||||
#' After that, \code{handlerExpr} will run every time that \code{eventExpr}
|
||||
#' is not \code{NULL}.
|
||||
#' This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
#' \emph{not} run when the \code{observeEvent}/\code{eventReactive} is
|
||||
#' created (because \code{ignoreInit = TRUE}). After that,
|
||||
#' \code{handlerExpr}/\code{valueExpr} will run every time that
|
||||
#' \code{eventExpr} is not \code{NULL}.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
@@ -1974,35 +2003,80 @@ observeEvent <- function(eventExpr, handlerExpr,
|
||||
initialized <- FALSE
|
||||
|
||||
o <- observe({
|
||||
e <- eventFunc()
|
||||
hybrid_chain(
|
||||
{eventFunc()},
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(e)) {
|
||||
return()
|
||||
}
|
||||
if (once) {
|
||||
on.exit(o$destroy())
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(o$destroy())
|
||||
}
|
||||
|
||||
isolate(handlerFunc())
|
||||
isolate(handlerFunc())
|
||||
}
|
||||
)
|
||||
}, label = label, suspended = suspended, priority = priority, domain = domain,
|
||||
autoDestroy = TRUE, ..stacktraceon = FALSE)
|
||||
|
||||
invisible(o)
|
||||
}
|
||||
|
||||
#' @section \code{eventReactive} caching:
|
||||
#'
|
||||
#' Like regular \code{\link{reactive}} expressions, the most recent value of a
|
||||
#' \code{eventReactive} is always cached. (Observers are not cached because
|
||||
#' they are used for their side-effects, not their values.) If a
|
||||
#' \code{reactive} or \code{eventReactive} named \code{r} is called with
|
||||
#' \code{r()} and then called again (without being invalidated in between),
|
||||
#' then the second call will simply return the most recent value.
|
||||
#'
|
||||
#' An \code{eventReactive} allows for caching of previous values, by using the
|
||||
#' \code{cache} parameter. When this additional caching is used, a key-value
|
||||
#' store is used, where the result of the \code{eventExpr} is used as the key.
|
||||
#' More specifically, the result from the \code{eventExpr} is combined with
|
||||
#' the \code{eventReactive}'s \code{label} (which defaults to a string
|
||||
#' representation of the \code{expr} code), and they are serialized and hashed
|
||||
#' to generate the key.
|
||||
#'
|
||||
#' When an additional cache is used, it allow for sharing cached values with
|
||||
#' other sessions. If you use \code{cache="session"}, then a separate cache
|
||||
#' will be used for each user session. If you use \code{cache="app"}, then the
|
||||
#' cache for the \code{eventReactive} will be shared across multiple client
|
||||
#' sessions accessing the same Shiny application -- because the \code{label}
|
||||
#' will (by default) be the same when the \code{expr} code is the same, an
|
||||
#' \code{eventReactive} in one session can share values with the corresponding
|
||||
#' \code{eventReactive} in another session. Whenever they have the same result
|
||||
#' for \code{eventExpr}, the value can be drawn from the cache instead of
|
||||
#' being recomputed.
|
||||
#'
|
||||
#' Other types of caching are possible, by passing a cache object with
|
||||
#' \code{$get()} and \code{$set()} methods. It is possible to cache the values
|
||||
#' to disk, or in an external database, and have the cache persist across
|
||||
#' application restarts. See \code{\link{renderCachedPlot}} for more
|
||||
#' information about caching with Shiny.
|
||||
#'
|
||||
#'
|
||||
#' @param cache Extra caching to use for \code{eventReactive}. Note that the
|
||||
#' most recent value is always cached, but this option allows you to cache
|
||||
#' previous values based on the value of \code{eventExpr}. If \code{NULL} (the
|
||||
#' default), do not use extra caching. Other possible values are \code{"app"}
|
||||
#' for an application-level cache, \code{"session"} for a session-level cache,
|
||||
#' or a cache object with \code{$get()} and \code{$set()} methods. See
|
||||
#' \code{\link{renderCachedPlot}} for more information about using caching.
|
||||
#' @rdname observeEvent
|
||||
#' @export
|
||||
eventReactive <- function(eventExpr, valueExpr,
|
||||
event.env = parent.frame(), event.quoted = FALSE,
|
||||
value.env = parent.frame(), value.quoted = FALSE,
|
||||
label = NULL, domain = getDefaultReactiveDomain(),
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, cache = NULL) {
|
||||
|
||||
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
|
||||
if (is.null(label))
|
||||
@@ -2014,17 +2088,64 @@ eventReactive <- function(eventExpr, valueExpr,
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
invisible(reactive({
|
||||
e <- eventFunc()
|
||||
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
ensureCacheSetup <- function() {
|
||||
# For our purposes, cache objects must support these methods.
|
||||
isCacheObject <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(e))
|
||||
if (is.null(cache)) {
|
||||
# No cache
|
||||
return()
|
||||
|
||||
isolate(handlerFunc())
|
||||
} else if (isCacheObject(cache)) {
|
||||
# If `cache` is already a cache object, do nothing
|
||||
return()
|
||||
|
||||
} else if (identical(cache, "app")) {
|
||||
cache <<- getShinyOption("cache")
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <<- session$getCache()
|
||||
|
||||
} else {
|
||||
stop('`cache` must either be NULL, "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
}
|
||||
ensureCacheSetup()
|
||||
|
||||
|
||||
invisible(reactive({
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
if (is.null(cache)) {
|
||||
return( isolate(handlerFunc()) )
|
||||
|
||||
} else {
|
||||
key <- digest::digest(list(value, label), "sha256")
|
||||
cached_value <- cache$get(key)
|
||||
if (!is.key_missing(cached_value)) {
|
||||
return(cached_value)
|
||||
}
|
||||
|
||||
result <- isolate(handlerFunc())
|
||||
cache$set(key, result)
|
||||
return(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}, label = label, domain = domain, ..stacktraceon = FALSE))
|
||||
}
|
||||
|
||||
|
||||
582
R/render-cached-plot.R
Normal file
582
R/render-cached-plot.R
Normal file
@@ -0,0 +1,582 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk.
|
||||
#'
|
||||
#' \code{expr} is an expression that generates a plot, similar to that in
|
||||
#' \code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
|
||||
#' take reactive dependencies. It is re-executed only when the cache key
|
||||
#' changes.
|
||||
#'
|
||||
#' \code{cacheKeyExpr} is an expression which, when evaluated, returns an object
|
||||
#' which will be serialized and hashed using the \code{\link[digest]{digest}}
|
||||
#' function to generate a string that will be used as a cache key. This key is
|
||||
#' used to identify the contents of the plot: if the cache key is the same as a
|
||||
#' previous time, it assumes that the plot is the same and can be retrieved from
|
||||
#' the cache.
|
||||
#'
|
||||
#' This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
|
||||
#' upstream reactives are invalidated. This will also trigger re-execution of
|
||||
#' the plotting expression, \code{expr}.
|
||||
#'
|
||||
#' The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
#' should in turn contain other normal R objects. If the key contains
|
||||
#' environments, external pointers, or reference objects -- or even if it has
|
||||
#' such objects attached as attributes -- then it is possible that it will
|
||||
#' change unpredictably even when you do not expect it to. Additionally, because
|
||||
#' the entire key is serialized and hashed, if it contains a very large object
|
||||
#' -- a large data set, for example -- there may be a noticeable performance
|
||||
#' penalty.
|
||||
#'
|
||||
#' If you face these issues with the cache key, you can work around them by
|
||||
#' extracting out the important parts of the objects, and/or by converting them
|
||||
#' to normal R objects before returning them. Your expression could even
|
||||
#' serialize and hash that information in an efficient way and return a string,
|
||||
#' which will in turn be hashed (very quickly) by the
|
||||
#' \code{\link[digest]{digest}} function.
|
||||
#'
|
||||
#' Internally, the result from \code{cacheKeyExpr} is combined with the name of
|
||||
#' the output (if you assign it to \code{output$plot1}, it will be combined
|
||||
#' with \code{"plot1"}) to form the actual key that is used. As a result, even
|
||||
#' if there are multiple plots that have the same \code{cacheKeyExpr}, they
|
||||
#' will not have cache key collisions.
|
||||
#'
|
||||
#' @section Cache scoping:
|
||||
#'
|
||||
#' There are a number of different ways you may want to scope the cache. For
|
||||
#' example, you may want each user session to have their own plot cache, or
|
||||
#' you may want each run of the application to have a cache (shared among
|
||||
#' possibly multiple simultaneous user sessions), or you may want to have a
|
||||
#' cache that persists even after the application is shut down and started
|
||||
#' again.
|
||||
#'
|
||||
#' To control the scope of the cache, use the \code{cache} parameter. There
|
||||
#' are two ways of having Shiny automatically create and clean up the disk
|
||||
#' cache.
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
#' among possibly multiple user sessions), use \code{cache="app"}. This
|
||||
#' is the default. The cache will be shared across multiple sessions, so
|
||||
#' there is potentially a large performance benefit if there are many users
|
||||
#' of the application. When the application stops running, the cache will
|
||||
#' be deleted. If plots cannot be safely shared across users, this should
|
||||
#' not be used.}
|
||||
#' \item{2}{To scope the cache to one session, use \code{cache="session"}.
|
||||
#' When a new user session starts -- in other words, when a web browser
|
||||
#' visits the Shiny application -- a new cache will be created on disk
|
||||
#' for that session. When the session ends, the cache will be deleted.
|
||||
#' The cache will not be shared across multiple sessions.}
|
||||
#' }
|
||||
#'
|
||||
#' If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
|
||||
#' in size, and will be stored stored in memory, using a
|
||||
#' \code{\link{memoryCache}} object. Note that the cache space will be shared
|
||||
#' among all cached plots within a single application or session.
|
||||
#'
|
||||
#' In some cases, you may want more control over the caching behavior. For
|
||||
#' example, you may want to use a larger or smaller cache, share a cache
|
||||
#' among multiple R processes, or you may want the cache to persist across
|
||||
#' multiple runs of an application, or even across multiple R processes.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' \code{\link{shinyOptions}()} at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 20 MB of space
|
||||
#' instead of the default 10 MB:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#' }
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can call
|
||||
#' \code{\link{shinyOptions}()} at the top of your server function. To use
|
||||
#' the session-scoped cache, you must also call \code{renderCachedPlot} with
|
||||
#' \code{cache="session"}. This will create a 20 MB cache for the session:
|
||||
#' \preformatted{
|
||||
#' function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' If you want to create a cache that is shared across multiple concurrent
|
||||
#' R processes, you can use a \code{\link{diskCache}}. You can create an
|
||||
#' application-level shared cache by putting this at the top of your app.R,
|
||||
#' server.R, or global.R:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' \code{myapp-cache} (replace \code{myapp-cache} with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
#' create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
|
||||
#' as the \code{cache} argument of \code{renderCachedPlot}.
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @param cacheKeyExpr An expression that returns a cache key. This key should
|
||||
#' be a unique identifier for a plot: the assumption is that if the cache key
|
||||
#' is the same, then the plot will be the same.
|
||||
#' @param sizePolicy A function that takes two arguments, \code{width} and
|
||||
#' \code{height}, and returns a list with \code{width} and \code{height}. The
|
||||
#' purpose is to round the actual pixel dimensions from the browser to some
|
||||
#' other dimensions, so that this will not generate and cache images of every
|
||||
#' possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
|
||||
#' information on the default sizing policy.
|
||||
#' @param res The resolution of the PNG, in pixels per inch.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be
|
||||
#' \code{"app"} (the default), \code{"session"}, or a cache object like
|
||||
#' a \code{\link{diskCache}}. See the Cache Scoping section for more
|
||||
#' information.
|
||||
#'
|
||||
#' @seealso See \code{\link{renderPlot}} for the regular, non-cached version of
|
||||
#' this function. For more about configuring caches, see
|
||||
#' \code{\link{memoryCache}} and \code{\link{diskCache}}.
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # A basic example that uses the default app-scoped memory cache.
|
||||
#' # The cache will be shared among all simultaneous users of the application.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(plotOutput("plot"))
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) }
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # An example uses a data object shared across sessions. mydata() is part of
|
||||
#' # the cache key, so when its value changes, plots that were previously
|
||||
#' # stored in the cache will no longer be used (unless mydata() changes back
|
||||
#' # to its previous value).
|
||||
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
#' actionButton("newdata", "New data")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$newdata, {
|
||||
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#' })
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' {
|
||||
#' Sys.sleep(2)
|
||||
#' d <- mydata()
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n, mydata()) },
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # A basic application with two plots, where each plot in each session has
|
||||
#' # a separate cache.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot1 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' \dontrun{
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a memory
|
||||
#' # cache that is 20 MB in size, and where cached objects expire after one
|
||||
#' # hour.
|
||||
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and is
|
||||
#' # deleted when the system reboots.
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and
|
||||
#' # persists on disk across reboots.
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#'
|
||||
#' # At the top of the server function, this set the session-scoped cache to be
|
||||
#' # a memory cache that is 5 MB in size.
|
||||
#' server <- function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
renderCachedPlot <- function(expr,
|
||||
cacheKeyExpr,
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
|
||||
res = 72,
|
||||
cache = "app",
|
||||
...,
|
||||
outputArgs = list()
|
||||
) {
|
||||
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
|
||||
# This is so that the expr doesn't re-execute by itself; it needs to be
|
||||
# triggered by the cache key (or width/height) changing.
|
||||
isolatedFunc <- function() isolate(func())
|
||||
|
||||
args <- list(...)
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
# The real cache key we'll use also includes width, height, res, pixelratio.
|
||||
# This is just the part supplied by the user.
|
||||
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
|
||||
|
||||
ensureCacheSetup <- function() {
|
||||
# For our purposes, cache objects must support these methods.
|
||||
isCacheObject <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (isCacheObject(cache)) {
|
||||
# If `cache` is already a cache object, do nothing
|
||||
return()
|
||||
|
||||
} else if (identical(cache, "app")) {
|
||||
cache <<- getShinyOption("cache")
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <<- session$cache
|
||||
|
||||
} else {
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
}
|
||||
|
||||
# The width and height of the plot to draw, given from sizePolicy. These
|
||||
# values get filled by an observer below.
|
||||
fitDims <- reactiveValues(width = NULL, height = NULL)
|
||||
|
||||
resizeObserver <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (!is.null(resizeObserver))
|
||||
return()
|
||||
|
||||
# Given the actual width/height of the image in the browser, this gets the
|
||||
# width/height from sizePolicy() and pushes those values into `fitDims`.
|
||||
# It's done this way so that the `fitDims` only change (and cause
|
||||
# invalidations) when the rendered image size changes, and not every time
|
||||
# the browser's <img> tag changes size.
|
||||
doResizeCheck <- function() {
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]]
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]]
|
||||
|
||||
if (is.null(width)) width <- 0
|
||||
if (is.null(height)) height <- 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims$width <- rect[1]
|
||||
fitDims$height <- rect[2]
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
resizeObserver <<- observe(doResizeCheck())
|
||||
}
|
||||
|
||||
# Vars to store session and output, so that they can be accessed from
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
|
||||
|
||||
drawReactive <- reactive(label = "plotObj", {
|
||||
hybrid_chain(
|
||||
# Depend on the user cache key, even though we don't use the value. When
|
||||
# it changes, it can cause the drawReactive to re-execute. (Though
|
||||
# drawReactive will not necessarily re-execute -- it must be called from
|
||||
# renderFunc, which happens only if there's a cache miss.)
|
||||
userCacheKey(),
|
||||
function(userCacheKeyValue) {
|
||||
# Get width/height, but don't depend on them.
|
||||
isolate({
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
})
|
||||
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = isolatedFunc,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
),
|
||||
args
|
||||
))
|
||||
},
|
||||
catch = function(reason) {
|
||||
# Non-isolating read. A common reason for errors in plotting is because
|
||||
# the dimensions are too small. By taking a dependency on width/height,
|
||||
# we can try again if the plot output element changes size.
|
||||
fitDims$width
|
||||
fitDims$height
|
||||
|
||||
# Propagate the error
|
||||
stop(reason)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
ensureCacheSetup()
|
||||
ensureResizeObserver()
|
||||
|
||||
hybrid_chain(
|
||||
# This use of the userCacheKey() sets up the reactive dependency that
|
||||
# causes plot re-draw events. These may involve pulling from the cache,
|
||||
# replaying a display list, or re-executing user code.
|
||||
userCacheKey(),
|
||||
function(userCacheKeyResult) {
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "sha256")
|
||||
|
||||
plotObj <- cache$get(key)
|
||||
|
||||
# First look in cache.
|
||||
# Case 1. cache hit.
|
||||
if (!is.key_missing(plotObj)) {
|
||||
return(list(
|
||||
cacheHit = TRUE,
|
||||
key = key,
|
||||
plotObj = plotObj,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
))
|
||||
}
|
||||
|
||||
# If not in cache, hybrid_chain call to drawReactive
|
||||
#
|
||||
# Two more possible cases:
|
||||
# 2. drawReactive will re-execute and return a plot that's the
|
||||
# correct size.
|
||||
# 3. It will not re-execute, but it will return the previous value,
|
||||
# which is the wrong size. It will include a valid display list
|
||||
# which can be used by resizeSavedPlot.
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(drawReactiveResult) {
|
||||
# Pass along the key for caching in the next stage
|
||||
list(
|
||||
cacheHit = FALSE,
|
||||
key = key,
|
||||
plotObj = drawReactiveResult,
|
||||
width = width,
|
||||
height = height,
|
||||
pixelratio = pixelratio
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
function(result) {
|
||||
width <- result$width
|
||||
height <- result$height
|
||||
pixelratio <- result$pixelratio
|
||||
|
||||
# Three possibilities when we get here:
|
||||
# 1. There was a cache hit. No need to set a value in the cache.
|
||||
# 2. There was a cache miss, and the plotObj is already the correct
|
||||
# size (because drawReactive re-executed). In this case, we need
|
||||
# to cache it.
|
||||
# 3. There was a cache miss, and the plotObj was not the corect size.
|
||||
# In this case, we need to replay the display list, and then cache
|
||||
# the result.
|
||||
if (!result$cacheHit) {
|
||||
# If the image is already the correct size, this just returns the
|
||||
# object unchanged.
|
||||
result$plotObj <- do.call("resizeSavedPlot", c(
|
||||
list(
|
||||
name,
|
||||
shinysession,
|
||||
result$plotObj,
|
||||
width,
|
||||
height,
|
||||
pixelratio,
|
||||
res
|
||||
),
|
||||
args
|
||||
))
|
||||
|
||||
# Save a cached copy of the plotObj. The recorded displaylist for
|
||||
# the plot can't be serialized and restored properly within the same
|
||||
# R session, so we NULL it out before saving. (The image data and
|
||||
# other metadata be saved and restored just fine.) Displaylists can
|
||||
# also be very large (~1.5MB for a basic ggplot), and they would not
|
||||
# be commonly used. Note that displaylist serialization was fixed in
|
||||
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
|
||||
# doesn't need to serialize objects, so it could actually save a
|
||||
# display list, but for the reasons listed previously, it's
|
||||
# generally not worth it.
|
||||
# The plotResult is not the same as the recordedPlot (it is used to
|
||||
# retrieve coordmap information for ggplot2 objects) but it is only
|
||||
# used in conjunction with the recordedPlot, and we'll remove it
|
||||
# because it can be quite large.
|
||||
result$plotObj$plotResult <- NULL
|
||||
result$plotObj$recordedPlot <- NULL
|
||||
cache$set(result$key, result$plotObj)
|
||||
}
|
||||
|
||||
img <- result$plotObj$img
|
||||
# Replace exact pixel dimensions; instead, the max-height and
|
||||
# max-width will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# If renderPlot isn't going to adapt to the height of the div, then the
|
||||
# div needs to adapt to the height of renderPlot. By default, plotOutput
|
||||
# sets the height to 400px, so to make it adapt we need to override it
|
||||
# with NULL.
|
||||
outputFunc <- plotOutput
|
||||
formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
|
||||
#' Create a sizing function that grows at a given ratio
|
||||
#'
|
||||
#' Returns a function which takes a two-element vector representing an input
|
||||
#' width and height, and returns a two-element vector of width and height. The
|
||||
#' possible widths are the base width times the growthRate to any integer power.
|
||||
#' For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
#' widths.
|
||||
#'
|
||||
#' @param width,height Base width and height.
|
||||
#' @param growthRate Growth rate multiplier.
|
||||
#'
|
||||
#' @seealso This is to be used with \code{\link{renderCachedPlot}}.
|
||||
#'
|
||||
#' @examples
|
||||
#' f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
#' f(c(400, 400))
|
||||
#' f(c(500, 500))
|
||||
#' f(c(530, 550))
|
||||
#' f(c(625, 700))
|
||||
#'
|
||||
#' @export
|
||||
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
|
||||
round_dim_up <- function(x, base, rate) {
|
||||
power <- ceiling(log(x / base, rate))
|
||||
ceiling(base * rate^power)
|
||||
}
|
||||
|
||||
function(dims) {
|
||||
if (length(dims) != 2) {
|
||||
stop("dims must be a vector with two numbers, for width and height.")
|
||||
}
|
||||
c(
|
||||
round_dim_up(dims[1], width, growthRate),
|
||||
round_dim_up(dims[2], height, growthRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
378
R/render-plot.R
378
R/render-plot.R
@@ -55,37 +55,20 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
|
||||
args <- list(...)
|
||||
|
||||
if (is.function(width))
|
||||
if (is.reactive(width))
|
||||
widthWrapper <- width
|
||||
else if (is.function(width))
|
||||
widthWrapper <- reactive({ width() })
|
||||
else
|
||||
widthWrapper <- function() { width }
|
||||
|
||||
if (is.function(height))
|
||||
if (is.reactive(height))
|
||||
heightWrapper <- height
|
||||
else if (is.function(height))
|
||||
heightWrapper <- reactive({ height() })
|
||||
else
|
||||
heightWrapper <- function() { height }
|
||||
|
||||
# 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
|
||||
# to be a (pseudo) S3 method is so that, if an object has a class in
|
||||
# addition to ggplot, and there's a print method for that class, that we
|
||||
# won't override that method. https://github.com/rstudio/shiny/issues/841
|
||||
print.ggplot <- function(x) {
|
||||
grid::grid.newpage()
|
||||
|
||||
build <- ggplot2::ggplot_build(x)
|
||||
|
||||
gtable <- ggplot2::ggplot_gtable(build)
|
||||
grid::grid.draw(gtable)
|
||||
|
||||
structure(list(
|
||||
build = build,
|
||||
gtable = gtable
|
||||
), class = "ggplot_build_gtable")
|
||||
}
|
||||
|
||||
|
||||
getDims <- function() {
|
||||
width <- widthWrapper()
|
||||
height <- heightWrapper()
|
||||
@@ -106,155 +89,59 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned. The expression
|
||||
# passed to renderPlot() is actually run in plotObj(); this function can only
|
||||
# replay a plot if the width/height changes.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
session <<- shinysession
|
||||
outputName <<- name
|
||||
# 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
|
||||
# reactive can be used for varying width/heights, as it includes the
|
||||
# displaylist, which is resolution independent.
|
||||
drawReactive <- reactive(label = "plotObj", {
|
||||
hybrid_chain(
|
||||
{
|
||||
# If !execOnResize, don't invalidate when width/height changes.
|
||||
dims <- if (execOnResize) getDims() else isolate(getDims())
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = func,
|
||||
width = dims$width,
|
||||
height = dims$height,
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
), args))
|
||||
},
|
||||
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.
|
||||
getDims()
|
||||
|
||||
dims <- getDims()
|
||||
|
||||
if (is.null(dims$width) || is.null(dims$height) ||
|
||||
dims$width <= 0 || dims$height <= 0) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
# The reactive that runs the expr in renderPlot()
|
||||
plotData <- plotObj()
|
||||
|
||||
img <- plotData$img
|
||||
|
||||
# If only the width/height have changed, simply replay the plot and make a
|
||||
# new img.
|
||||
if (dims$width != img$width || dims$height != img$height) {
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
coordmap <- NULL
|
||||
plotFunc <- function() {
|
||||
..stacktraceon..(grDevices::replayPlot(plotData$recordedPlot))
|
||||
|
||||
# Coordmap must be recalculated after replaying plot, because pixel
|
||||
# dimensions will have changed.
|
||||
if (inherits(plotData$plotResult, "ggplot_build_gtable")) {
|
||||
coordmap <<- getGgplotCoordmap(plotData$plotResult, pixelratio, res)
|
||||
} else {
|
||||
coordmap <<- getPrevPlotCoordmap(dims$width, dims$height)
|
||||
}
|
||||
# Propagate the error
|
||||
stop(reason)
|
||||
}
|
||||
outfile <- ..stacktraceoff..(
|
||||
plotPNG(plotFunc, width = dims$width*pixelratio, height = dims$height*pixelratio,
|
||||
res = res*pixelratio)
|
||||
)
|
||||
on.exit(unlink(outfile))
|
||||
|
||||
img <- dropNulls(list(
|
||||
src = session$fileUrl(name, outfile, contentType='image/png'),
|
||||
width = dims$width,
|
||||
height = dims$height,
|
||||
coordmap = coordmap,
|
||||
# Get coordmap error message if present
|
||||
error = attr(coordmap, "error", exact = TRUE)
|
||||
))
|
||||
}
|
||||
|
||||
img
|
||||
}
|
||||
|
||||
|
||||
plotObj <- reactive(label = "plotObj", {
|
||||
if (execOnResize) {
|
||||
dims <- getDims()
|
||||
} else {
|
||||
isolate({ dims <- getDims() })
|
||||
}
|
||||
|
||||
if (is.null(dims$width) || is.null(dims$height) ||
|
||||
dims$width <= 0 || dims$height <= 0) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
# Resolution multiplier
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
plotResult <- NULL
|
||||
recordedPlot <- NULL
|
||||
coordmap <- NULL
|
||||
plotFunc <- function() {
|
||||
success <-FALSE
|
||||
tryCatch(
|
||||
{
|
||||
# This is necessary to enable displaylist recording
|
||||
grDevices::dev.control(displaylist = "enable")
|
||||
|
||||
# Actually perform the plotting
|
||||
result <- withVisible(func())
|
||||
success <- TRUE
|
||||
},
|
||||
finally = {
|
||||
if (!success) {
|
||||
# If there was an error in making the plot, there's a good chance
|
||||
# it's "Error in plot.new: figure margins too large". We need to
|
||||
# take a reactive dependency on the width and height, so that the
|
||||
# user's plotting code will re-execute when the plot is resized,
|
||||
# instead of just replaying the previous plot (which errored).
|
||||
getDims()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (result$visible) {
|
||||
# Use capture.output to squelch printing to the actual console; we
|
||||
# are only interested in plot output
|
||||
utils::capture.output({
|
||||
# This ..stacktraceon.. negates the ..stacktraceoff.. that wraps
|
||||
# the call to plotFunc. The value needs to be printed just in case
|
||||
# it's an object that requires printing to generate plot output,
|
||||
# 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.
|
||||
plotResult <<- ..stacktraceon..(print(result$value))
|
||||
})
|
||||
}
|
||||
|
||||
recordedPlot <<- grDevices::recordPlot()
|
||||
|
||||
if (inherits(plotResult, "ggplot_build_gtable")) {
|
||||
coordmap <<- getGgplotCoordmap(plotResult, pixelratio, res)
|
||||
} else {
|
||||
coordmap <<- getPrevPlotCoordmap(dims$width, dims$height)
|
||||
}
|
||||
}
|
||||
|
||||
# This ..stacktraceoff.. is matched by the `func` function's
|
||||
# wrapFunctionLabel(..stacktraceon=TRUE) call near the beginning of
|
||||
# renderPlot, and by the ..stacktraceon.. in plotFunc where ggplot objects
|
||||
# are printed
|
||||
outfile <- ..stacktraceoff..(
|
||||
do.call(plotPNG, c(plotFunc, width=dims$width*pixelratio,
|
||||
height=dims$height*pixelratio, res=res*pixelratio, args))
|
||||
)
|
||||
on.exit(unlink(outfile))
|
||||
|
||||
list(
|
||||
# img is the content that gets sent to the client.
|
||||
img = dropNulls(list(
|
||||
src = session$fileUrl(outputName, outfile, contentType='image/png'),
|
||||
width = dims$width,
|
||||
height = dims$height,
|
||||
coordmap = coordmap,
|
||||
# Get coordmap error message if present.
|
||||
error = attr(coordmap, "error", exact = TRUE)
|
||||
)),
|
||||
# Returned value from expression in renderPlot() -- may be a printable
|
||||
# object like ggplot2. Needed just in case we replayPlot and need to get
|
||||
# a coordmap again.
|
||||
plotResult = plotResult,
|
||||
recordedPlot = recordedPlot
|
||||
)
|
||||
})
|
||||
|
||||
# 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
|
||||
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(result) {
|
||||
dims <- getDims()
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
result <- do.call("resizeSavedPlot", c(
|
||||
list(name, shinysession, result, dims$width, dims$height, pixelratio, res),
|
||||
args
|
||||
))
|
||||
|
||||
result$img
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -266,6 +153,133 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
|
||||
if (result$img$width == width && result$img$height == height &&
|
||||
result$pixelratio == pixelratio && result$res == res) {
|
||||
return(result)
|
||||
}
|
||||
|
||||
coordmap <- NULL
|
||||
outfile <- plotPNG(function() {
|
||||
grDevices::replayPlot(result$recordedPlot)
|
||||
coordmap <<- getCoordmap(result$plotResult, width, height, pixelratio, res)
|
||||
}, width = width*pixelratio, height = height*pixelratio, res = res*pixelratio, ...)
|
||||
on.exit(unlink(outfile), add = TRUE)
|
||||
|
||||
result$img <- list(
|
||||
src = session$fileUrl(name, outfile, contentType = "image/png"),
|
||||
width = width,
|
||||
height = height,
|
||||
coordmap = coordmap,
|
||||
error = attr(coordmap, "error", exact = TRUE)
|
||||
)
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
|
||||
# 1. Start PNG
|
||||
# 2. Enable displaylist recording
|
||||
# 3. Call user-defined func
|
||||
# 4. Print/save result, if visible
|
||||
# 5. Snapshot displaylist
|
||||
# 6. Form coordmap
|
||||
# 7. End PNG (in finally)
|
||||
# 8. Form img tag
|
||||
# 9. Return img, value, displaylist, coordmap
|
||||
# 10. On error, take width and height dependency
|
||||
|
||||
outfile <- tempfile(fileext='.png') # If startPNG throws, this could leak. Shrug.
|
||||
device <- startPNG(outfile, width*pixelratio, height*pixelratio, res = res*pixelratio, ...)
|
||||
domain <- createGraphicsDevicePromiseDomain(device)
|
||||
grDevices::dev.control(displaylist = "enable")
|
||||
|
||||
hybrid_chain(
|
||||
hybrid_chain(
|
||||
promises::with_promise_domain(domain, {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
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
|
||||
# to be a (pseudo) S3 method is so that, if an object has a class in
|
||||
# addition to ggplot, and there's a print method for that class, that we
|
||||
# won't override that method. https://github.com/rstudio/shiny/issues/841
|
||||
print.ggplot <- custom_print.ggplot
|
||||
|
||||
# Use capture.output to squelch printing to the actual console; we
|
||||
# are only interested in plot output
|
||||
utils::capture.output({
|
||||
# This ..stacktraceon.. negates the ..stacktraceoff.. that wraps
|
||||
# the call to plotFunc. The value needs to be printed just in case
|
||||
# it's an object that requires printing to generate plot output,
|
||||
# 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(value))
|
||||
# TODO jcheng 2017-04-11: Verify above ..stacktraceon..
|
||||
})
|
||||
result
|
||||
} else {
|
||||
# Not necessary, but I wanted to make it explicit
|
||||
NULL
|
||||
}
|
||||
},
|
||||
function(value) {
|
||||
list(
|
||||
plotResult = value,
|
||||
recordedPlot = grDevices::recordPlot(),
|
||||
coordmap = getCoordmap(value, width, height, pixelratio, res),
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
)
|
||||
}
|
||||
)
|
||||
}),
|
||||
finally = function() {
|
||||
grDevices::dev.off(device)
|
||||
}
|
||||
),
|
||||
function(result) {
|
||||
result$img <- dropNulls(list(
|
||||
src = session$fileUrl(name, outfile, contentType='image/png'),
|
||||
width = width,
|
||||
height = height,
|
||||
coordmap = result$coordmap,
|
||||
# Get coordmap error message if present
|
||||
error = attr(result$coordmap, "error", exact = TRUE)
|
||||
))
|
||||
|
||||
result
|
||||
},
|
||||
finally = function() {
|
||||
unlink(outfile)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# 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
|
||||
# to be a (pseudo) S3 method is so that, if an object has a class in
|
||||
# addition to ggplot, and there's a print method for that class, that we
|
||||
# won't override that method. https://github.com/rstudio/shiny/issues/841
|
||||
custom_print.ggplot <- function(x) {
|
||||
grid::grid.newpage()
|
||||
|
||||
build <- ggplot2::ggplot_build(x)
|
||||
|
||||
gtable <- ggplot2::ggplot_gtable(build)
|
||||
grid::grid.draw(gtable)
|
||||
|
||||
structure(list(
|
||||
build = build,
|
||||
gtable = gtable
|
||||
), class = "ggplot_build_gtable")
|
||||
}
|
||||
|
||||
# The coordmap extraction functions below return something like the examples
|
||||
# below. For base graphics:
|
||||
# plot(mtcars$wt, mtcars$mpg)
|
||||
@@ -384,6 +398,14 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
|
||||
# .. ..$ top : num 35.7
|
||||
|
||||
|
||||
getCoordmap <- function(x, width, height, pixelratio, res) {
|
||||
if (inherits(x, "ggplot_build_gtable")) {
|
||||
getGgplotCoordmap(x, pixelratio, res)
|
||||
} else {
|
||||
getPrevPlotCoordmap(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
# Get a coordmap for the previous plot made with base graphics.
|
||||
# Requires width and height of output image, in pixels.
|
||||
# Must be called before the graphics device is closed.
|
||||
@@ -408,10 +430,10 @@ getPrevPlotCoordmap <- function(width, height) {
|
||||
),
|
||||
# The bounds of the plot area, in DOM pixels
|
||||
range = list(
|
||||
left = graphics::grconvertX(usrBounds[1], 'user', 'nfc') * width,
|
||||
right = graphics::grconvertX(usrBounds[2], 'user', 'nfc') * width,
|
||||
bottom = (1-graphics::grconvertY(usrBounds[3], 'user', 'nfc')) * height - 1,
|
||||
top = (1-graphics::grconvertY(usrBounds[4], 'user', 'nfc')) * height - 1
|
||||
left = graphics::grconvertX(usrBounds[1], 'user', 'ndc') * width,
|
||||
right = graphics::grconvertX(usrBounds[2], 'user', 'ndc') * width,
|
||||
bottom = (1-graphics::grconvertY(usrBounds[3], 'user', 'ndc')) * height - 1,
|
||||
top = (1-graphics::grconvertY(usrBounds[4], 'user', 'ndc')) * height - 1
|
||||
),
|
||||
log = list(
|
||||
x = if (graphics::par('xlog')) 10 else NULL,
|
||||
@@ -424,7 +446,6 @@ getPrevPlotCoordmap <- function(width, height) {
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
# Given a ggplot_build_gtable object, return a coordmap for it.
|
||||
getGgplotCoordmap <- function(p, pixelratio, res) {
|
||||
if (!inherits(p, "ggplot_build_gtable"))
|
||||
@@ -539,9 +560,11 @@ find_panel_info_api <- function(b) {
|
||||
# ggplot object. The original uses quoted expressions; convert to
|
||||
# character.
|
||||
mapping <- layers$mapping[[1]]
|
||||
# lapply'ing as.character results in unexpected behavior for expressions
|
||||
# like `wt/2`; deparse handles it correctly.
|
||||
mapping <- lapply(mapping, deparse)
|
||||
# In ggplot2 <=2.2.1, the mappings are expressions. In later versions, they
|
||||
# are quosures. `deparse(quo_squash(x))` will handle both cases.
|
||||
# as.character results in unexpected behavior for expressions like `wt/2`,
|
||||
# which is why we use deparse.
|
||||
mapping <- lapply(mapping, function(x) deparse(rlang::quo_squash(x)))
|
||||
|
||||
# If either x or y is not present, give it a NULL entry.
|
||||
mapping <- mergeVectors(list(x = NULL, y = NULL), mapping)
|
||||
@@ -723,8 +746,9 @@ find_panel_info_non_api <- function(b, ggplot_format) {
|
||||
mappings <- c(list(mappings), layer_mappings)
|
||||
mappings <- Reduce(x = mappings, init = list(x = NULL, y = NULL),
|
||||
function(init, m) {
|
||||
if (is.null(init$x) && !is.null(m$x)) init$x <- m$x
|
||||
if (is.null(init$y) && !is.null(m$y)) init$y <- m$y
|
||||
# Can't use m$x/m$y; you get a partial match with xintercept/yintercept
|
||||
if (is.null(init[["x"]]) && !is.null(m[["x"]])) init$x <- m[["x"]]
|
||||
if (is.null(init[["y"]]) && !is.null(m[["y"]])) init$y <- m[["y"]]
|
||||
init
|
||||
}
|
||||
)
|
||||
|
||||
264
R/render-table.R
264
R/render-table.R
@@ -81,148 +81,148 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
|
||||
|
||||
dots <- list(...) ## used later (but defined here because of scoping)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
striped <- stripedWrapper()
|
||||
hover <- hoverWrapper()
|
||||
bordered <- borderedWrapper()
|
||||
format <- c(striped = striped, hover = hover, bordered = bordered)
|
||||
spacing <- spacingWrapper()
|
||||
width <- widthWrapper()
|
||||
align <- alignWrapper()
|
||||
rownames <- rownamesWrapper()
|
||||
colnames <- colnamesWrapper()
|
||||
digits <- digitsWrapper()
|
||||
na <- naWrapper()
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(data, session, name, ...) {
|
||||
striped <- stripedWrapper()
|
||||
hover <- hoverWrapper()
|
||||
bordered <- borderedWrapper()
|
||||
format <- c(striped = striped, hover = hover, bordered = bordered)
|
||||
spacing <- spacingWrapper()
|
||||
width <- widthWrapper()
|
||||
align <- alignWrapper()
|
||||
rownames <- rownamesWrapper()
|
||||
colnames <- colnamesWrapper()
|
||||
digits <- digitsWrapper()
|
||||
na <- naWrapper()
|
||||
|
||||
spacing_choices <- c("s", "xs", "m", "l")
|
||||
if (!(spacing %in% spacing_choices)) {
|
||||
stop(paste("`spacing` must be one of",
|
||||
paste0("'", spacing_choices, "'", collapse=", ")))
|
||||
}
|
||||
spacing_choices <- c("s", "xs", "m", "l")
|
||||
if (!(spacing %in% spacing_choices)) {
|
||||
stop(paste("`spacing` must be one of",
|
||||
paste0("'", spacing_choices, "'", collapse=", ")))
|
||||
}
|
||||
|
||||
# For css styling
|
||||
classNames <- paste0("table shiny-table",
|
||||
paste0(" table-", names(format)[format], collapse = "" ),
|
||||
paste0(" spacing-", spacing))
|
||||
# For css styling
|
||||
classNames <- paste0("table shiny-table",
|
||||
paste0(" table-", names(format)[format], collapse = "" ),
|
||||
paste0(" spacing-", spacing))
|
||||
|
||||
data <- func()
|
||||
data <- as.data.frame(data)
|
||||
data <- as.data.frame(data)
|
||||
|
||||
# Return NULL if no data is provided
|
||||
if (is.null(data) ||
|
||||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
|
||||
return(NULL)
|
||||
# Return NULL if no data is provided
|
||||
if (is.null(data) ||
|
||||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
|
||||
return(NULL)
|
||||
|
||||
# Separate the ... args to pass to xtable() vs print.xtable()
|
||||
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
|
||||
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
|
||||
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
|
||||
# Separate the ... args to pass to xtable() vs print.xtable()
|
||||
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
|
||||
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
|
||||
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
|
||||
|
||||
# By default, numbers are right-aligned and everything else is left-aligned.
|
||||
defaultAlignment <- function(col) {
|
||||
if (is.numeric(col)) "r" else "l"
|
||||
}
|
||||
# By default, numbers are right-aligned and everything else is left-aligned.
|
||||
defaultAlignment <- function(col) {
|
||||
if (is.numeric(col)) "r" else "l"
|
||||
}
|
||||
|
||||
# Figure out column alignment
|
||||
## Case 1: default alignment
|
||||
if (is.null(align) || align == "?") {
|
||||
names <- defaultAlignment(attr(data, "row.names"))
|
||||
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
|
||||
cols <- paste0(names, cols)
|
||||
} else {
|
||||
## Case 2: user-specified alignment
|
||||
num_cols <- if (rownames) nchar(align) else nchar(align)+1
|
||||
valid <- !grepl("[^lcr\\?]", align)
|
||||
if (num_cols == ncol(data)+1 && valid) {
|
||||
cols <- if (rownames) align else paste0("r", align)
|
||||
defaults <- grep("\\?", strsplit(cols,"")[[1]])
|
||||
if (length(defaults) != 0) {
|
||||
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
|
||||
for (i in seq_len(length(defaults))) {
|
||||
substr(cols, defaults[i], defaults[i]) <- vals[i]
|
||||
}
|
||||
}
|
||||
} else if (nchar(align) == 1 && valid) {
|
||||
cols <- paste0(rep(align, ncol(data)+1), collapse="")
|
||||
# Figure out column alignment
|
||||
## Case 1: default alignment
|
||||
if (is.null(align) || align == "?") {
|
||||
names <- defaultAlignment(attr(data, "row.names"))
|
||||
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
|
||||
cols <- paste0(names, cols)
|
||||
} else {
|
||||
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
|
||||
"have length either equal to 1 or to the total number of columns")
|
||||
## Case 2: user-specified alignment
|
||||
num_cols <- if (rownames) nchar(align) else nchar(align)+1
|
||||
valid <- !grepl("[^lcr\\?]", align)
|
||||
if (num_cols == ncol(data)+1 && valid) {
|
||||
cols <- if (rownames) align else paste0("r", align)
|
||||
defaults <- grep("\\?", strsplit(cols,"")[[1]])
|
||||
if (length(defaults) != 0) {
|
||||
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
|
||||
for (i in seq_len(length(defaults))) {
|
||||
substr(cols, defaults[i], defaults[i]) <- vals[i]
|
||||
}
|
||||
}
|
||||
} else if (nchar(align) == 1 && valid) {
|
||||
cols <- paste0(rep(align, ncol(data)+1), collapse="")
|
||||
} else {
|
||||
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
|
||||
"have length either equal to 1 or to the total number of columns")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Call xtable with its (updated) args
|
||||
xtable_args <- c(xtable_args, align = cols, digits = digits)
|
||||
xtable_res <- do.call(xtable, c(list(data), xtable_args))
|
||||
# Call xtable with its (updated) args
|
||||
xtable_args <- c(xtable_args, align = cols, digits = digits)
|
||||
xtable_res <- do.call(xtable, c(list(data), xtable_args))
|
||||
|
||||
# Set up print args
|
||||
print_args <- list(
|
||||
x = xtable_res,
|
||||
type = 'html',
|
||||
include.rownames = {
|
||||
if ("include.rownames" %in% names(dots)) dots$include.rownames
|
||||
else rownames
|
||||
},
|
||||
include.colnames = {
|
||||
if ("include.colnames" %in% names(dots)) dots$include.colnames
|
||||
else colnames
|
||||
},
|
||||
NA.string = {
|
||||
if ("NA.string" %in% names(dots)) dots$NA.string
|
||||
else na
|
||||
},
|
||||
html.table.attributes =
|
||||
paste0({
|
||||
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
|
||||
else ""
|
||||
}, " ",
|
||||
"class = '", htmlEscape(classNames, TRUE), "' ",
|
||||
"style = 'width:", validateCssUnit(width), ";'"),
|
||||
comment = {
|
||||
if ("comment" %in% names(dots)) dots$comment
|
||||
else FALSE
|
||||
# Set up print args
|
||||
print_args <- list(
|
||||
x = xtable_res,
|
||||
type = 'html',
|
||||
include.rownames = {
|
||||
if ("include.rownames" %in% names(dots)) dots$include.rownames
|
||||
else rownames
|
||||
},
|
||||
include.colnames = {
|
||||
if ("include.colnames" %in% names(dots)) dots$include.colnames
|
||||
else colnames
|
||||
},
|
||||
NA.string = {
|
||||
if ("NA.string" %in% names(dots)) dots$NA.string
|
||||
else na
|
||||
},
|
||||
html.table.attributes =
|
||||
paste0({
|
||||
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
|
||||
else ""
|
||||
}, " ",
|
||||
"class = '", htmlEscape(classNames, TRUE), "' ",
|
||||
"style = 'width:", validateCssUnit(width), ";'"),
|
||||
comment = {
|
||||
if ("comment" %in% names(dots)) dots$comment
|
||||
else FALSE
|
||||
}
|
||||
)
|
||||
|
||||
print_args <- c(print_args, non_xtable_args)
|
||||
print_args <- print_args[unique(names(print_args))]
|
||||
|
||||
# Capture the raw html table returned by print.xtable(), and store it in
|
||||
# a variable for further processing
|
||||
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
|
||||
|
||||
# Add extra class to cells with NA value, to be able to style them separately
|
||||
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
|
||||
|
||||
# All further processing concerns the table headers, so we don't need to run
|
||||
# any of this if colnames=FALSE
|
||||
if (colnames) {
|
||||
# Make sure that the final html table has a proper header (not included
|
||||
# in the print.xtable() default)
|
||||
tab <- sub("<tr>", "<thead> <tr>", tab)
|
||||
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
|
||||
tab <- sub("</table>$", "</tbody> </table>", tab)
|
||||
|
||||
# Update the `cols` string (which stores the alignment of each column) so
|
||||
# that it only includes the alignment for the table variables (and not
|
||||
# for the row.names)
|
||||
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
|
||||
|
||||
# Create a vector whose i-th entry corresponds to the i-th table variable
|
||||
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
|
||||
cols <- strsplit(cols, "")[[1]]
|
||||
cols[cols == "l"] <- "left"
|
||||
cols[cols == "r"] <- "right"
|
||||
cols[cols == "c"] <- "center"
|
||||
|
||||
# Align each header accordingly (this guarantees that each header and its
|
||||
# corresponding column have the same alignment)
|
||||
for (i in seq_len(length(cols))) {
|
||||
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
print_args <- c(print_args, non_xtable_args)
|
||||
print_args <- print_args[unique(names(print_args))]
|
||||
|
||||
# Capture the raw html table returned by print.xtable(), and store it in
|
||||
# a variable for further processing
|
||||
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
|
||||
|
||||
# Add extra class to cells with NA value, to be able to style them separately
|
||||
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
|
||||
|
||||
# All further processing concerns the table headers, so we don't need to run
|
||||
# any of this if colnames=FALSE
|
||||
if (colnames) {
|
||||
# Make sure that the final html table has a proper header (not included
|
||||
# in the print.xtable() default)
|
||||
tab <- sub("<tr>", "<thead> <tr>", tab)
|
||||
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
|
||||
tab <- sub("</table>$", "</tbody> </table>", tab)
|
||||
|
||||
# Update the `cols` string (which stores the alignment of each column) so
|
||||
# that it only includes the alignment for the table variables (and not
|
||||
# for the row.names)
|
||||
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
|
||||
|
||||
# Create a vector whose i-th entry corresponds to the i-th table variable
|
||||
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
|
||||
cols <- strsplit(cols, "")[[1]]
|
||||
cols[cols == "l"] <- "left"
|
||||
cols[cols == "r"] <- "right"
|
||||
cols[cols == "c"] <- "center"
|
||||
|
||||
# Align each header accordingly (this guarantees that each header and its
|
||||
# corresponding column have the same alignment)
|
||||
for (i in seq_len(length(cols))) {
|
||||
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
|
||||
}
|
||||
}
|
||||
return(tab)
|
||||
}
|
||||
|
||||
# Main render function
|
||||
markRenderFunction(tableOutput, renderFunc, outputArgs = outputArgs)
|
||||
return(tab)
|
||||
},
|
||||
tableOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ registerInputHandler("shiny.matrix", function(data, ...) {
|
||||
return(m)
|
||||
})
|
||||
|
||||
|
||||
registerInputHandler("shiny.number", function(val, ...){
|
||||
ifelse(is.null(val), NA, val)
|
||||
})
|
||||
@@ -220,3 +221,21 @@ registerInputHandler("shiny.file", function(val, shinysession, name) {
|
||||
|
||||
val
|
||||
})
|
||||
|
||||
|
||||
# to be used with !!!answer
|
||||
registerInputHandler("shiny.symbolList", function(val, ...) {
|
||||
if (is.null(val)) {
|
||||
list()
|
||||
} else {
|
||||
lapply(val, as.symbol)
|
||||
}
|
||||
})
|
||||
# to be used with !!answer
|
||||
registerInputHandler("shiny.symbol", function(val, ...) {
|
||||
if (is.null(val) || identical(val, "")) {
|
||||
NULL
|
||||
} else {
|
||||
as.symbol(val)
|
||||
}
|
||||
})
|
||||
|
||||
216
R/server.R
216
R/server.R
@@ -1,6 +1,7 @@
|
||||
#' @include server-input-handlers.R
|
||||
|
||||
appsByToken <- Map$new()
|
||||
appsNeedingFlush <- Map$new()
|
||||
|
||||
# Provide a character representation of the WS that can be used
|
||||
# as a key in a Map.
|
||||
@@ -52,21 +53,23 @@ registerClient <- function(client) {
|
||||
#' @export
|
||||
addResourcePath <- function(prefix, directoryPath) {
|
||||
prefix <- prefix[1]
|
||||
if (!grepl('^[a-z0-9\\-_][a-z0-9\\-_.]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
|
||||
if (!grepl('^[a-z0-9\\-_][a-z0-9\\-_.]*$', prefix, ignore.case = TRUE, perl = TRUE)) {
|
||||
stop("addResourcePath called with invalid prefix; please see documentation")
|
||||
}
|
||||
|
||||
if (prefix %in% c('shared')) {
|
||||
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
||||
"please use a different prefix")
|
||||
}
|
||||
|
||||
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
|
||||
|
||||
existing <- .globals$resources[[prefix]]
|
||||
|
||||
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
|
||||
func=staticHandler(directoryPath))
|
||||
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
|
||||
error = function(e) {
|
||||
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
|
||||
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
|
||||
}
|
||||
)
|
||||
.globals$resources[[prefix]] <- list(
|
||||
directoryPath = normalizedPath,
|
||||
func = staticHandler(normalizedPath)
|
||||
)
|
||||
}
|
||||
|
||||
resourcePathHandler <- function(req) {
|
||||
@@ -243,94 +246,87 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
} else {
|
||||
# If there's bookmarked state, save it on the session object
|
||||
shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
|
||||
shinysession$createBookmarkObservers()
|
||||
}
|
||||
}
|
||||
|
||||
withRestoreContext(shinysession$restoreContext, {
|
||||
|
||||
msg$data <- applyInputHandlers(msg$data)
|
||||
msg$data <- applyInputHandlers(msg$data)
|
||||
|
||||
switch(
|
||||
msg$method,
|
||||
init = {
|
||||
switch(
|
||||
msg$method,
|
||||
init = {
|
||||
|
||||
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
||||
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
|
||||
appvars$server <- serverFunc
|
||||
if (!is.null(appvars$server))
|
||||
{
|
||||
# Tag this function as the Shiny server function. A debugger may use this
|
||||
# tag to give this function special treatment.
|
||||
# It's very important that it's appvars$server itself and NOT a copy that
|
||||
# is invoked, otherwise new breakpoints won't be picked up.
|
||||
attr(appvars$server, "shinyServerFunction") <- TRUE
|
||||
registerDebugHook("server", appvars, "Server Function")
|
||||
}
|
||||
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
||||
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
|
||||
appvars$server <- serverFunc
|
||||
if (!is.null(appvars$server))
|
||||
{
|
||||
# Tag this function as the Shiny server function. A debugger may use this
|
||||
# tag to give this function special treatment.
|
||||
# It's very important that it's appvars$server itself and NOT a copy that
|
||||
# is invoked, otherwise new breakpoints won't be picked up.
|
||||
attr(appvars$server, "shinyServerFunction") <- TRUE
|
||||
registerDebugHook("server", appvars, "Server Function")
|
||||
}
|
||||
}
|
||||
|
||||
# Check for switching into/out of showcase mode
|
||||
if (.globals$showcaseOverride &&
|
||||
exists(".clientdata_url_search", where = msg$data)) {
|
||||
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
|
||||
if (!is.null(mode))
|
||||
shinysession$setShowcase(mode)
|
||||
}
|
||||
# Check for switching into/out of showcase mode
|
||||
if (.globals$showcaseOverride &&
|
||||
exists(".clientdata_url_search", where = msg$data)) {
|
||||
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
|
||||
if (!is.null(mode))
|
||||
shinysession$setShowcase(mode)
|
||||
}
|
||||
|
||||
shinysession$manageInputs(msg$data)
|
||||
# In shinysession$createBookmarkObservers() above, observers may be
|
||||
# created, which puts the shiny session in busyCount > 0 state. That
|
||||
# prevents the manageInputs here from taking immediate effect, by
|
||||
# default. The manageInputs here needs to take effect though, because
|
||||
# otherwise the bookmark observers won't find the clientData they are
|
||||
# looking for. So use `now = TRUE` to force the changes to be
|
||||
# immediate.
|
||||
#
|
||||
# FIXME: break createBookmarkObservers into two separate steps, one
|
||||
# before and one after manageInputs, and put the observer creation
|
||||
# in the latter. Then add an assertion that busyCount == 0L when
|
||||
# this manageInputs is called.
|
||||
shinysession$manageInputs(msg$data, now = TRUE)
|
||||
|
||||
# The client tells us what singletons were rendered into
|
||||
# the initial page
|
||||
if (!is.null(msg$data$.clientdata_singletons)) {
|
||||
shinysession$singletons <- strsplit(
|
||||
msg$data$.clientdata_singletons, ',')[[1]]
|
||||
}
|
||||
# The client tells us what singletons were rendered into
|
||||
# the initial page
|
||||
if (!is.null(msg$data$.clientdata_singletons)) {
|
||||
shinysession$singletons <- strsplit(
|
||||
msg$data$.clientdata_singletons, ',')[[1]]
|
||||
}
|
||||
|
||||
local({
|
||||
args <- argsForServerFunc(serverFunc, shinysession)
|
||||
local({
|
||||
args <- argsForServerFunc(serverFunc, shinysession)
|
||||
|
||||
withReactiveDomain(shinysession, {
|
||||
do.call(
|
||||
# No corresponding ..stacktraceoff; the server func is pure
|
||||
# user code
|
||||
wrapFunctionLabel(appvars$server, "server",
|
||||
..stacktraceon = TRUE
|
||||
),
|
||||
args
|
||||
)
|
||||
})
|
||||
withReactiveDomain(shinysession, {
|
||||
do.call(
|
||||
# No corresponding ..stacktraceoff; the server func is pure
|
||||
# user code
|
||||
wrapFunctionLabel(appvars$server, "server",
|
||||
..stacktraceon = TRUE
|
||||
),
|
||||
args
|
||||
)
|
||||
})
|
||||
},
|
||||
update = {
|
||||
shinysession$manageInputs(msg$data)
|
||||
},
|
||||
shinysession$dispatch(msg)
|
||||
)
|
||||
shinysession$manageHiddenOutputs()
|
||||
})
|
||||
},
|
||||
update = {
|
||||
shinysession$manageInputs(msg$data)
|
||||
},
|
||||
shinysession$dispatch(msg)
|
||||
)
|
||||
# The HTTP_GUID, if it exists, is for Shiny Server reporting purposes
|
||||
shinysession$startTiming(ws$request$HTTP_GUID)
|
||||
shinysession$requestFlush()
|
||||
|
||||
if (exists(".shiny__stdout", globalenv()) &&
|
||||
exists("HTTP_GUID", ws$request)) {
|
||||
# safe to assume we're in shiny-server
|
||||
shiny_stdout <- get(".shiny__stdout", globalenv())
|
||||
|
||||
# eNter a flushReact
|
||||
writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
|
||||
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
||||
sep=""), con=shiny_stdout)
|
||||
flush(shiny_stdout)
|
||||
|
||||
flushReact()
|
||||
|
||||
# eXit a flushReact
|
||||
writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
|
||||
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
||||
sep=""), con=shiny_stdout)
|
||||
flush(shiny_stdout)
|
||||
} else {
|
||||
flushReact()
|
||||
}
|
||||
|
||||
flushAllSessions()
|
||||
})
|
||||
# Make httpuv return control to Shiny quickly, instead of waiting
|
||||
# for the usual timeout
|
||||
httpuv::interrupt()
|
||||
})
|
||||
}
|
||||
ws$onMessage(function(binary, msg) {
|
||||
@@ -341,6 +337,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
ws$onClose(function() {
|
||||
shinysession$wsClosed()
|
||||
appsByToken$remove(shinysession$token)
|
||||
appsNeedingFlush$remove(shinysession$token)
|
||||
})
|
||||
|
||||
return(TRUE)
|
||||
@@ -422,7 +419,10 @@ startApp <- function(appObj, port, host, quiet) {
|
||||
|
||||
if (is.numeric(port) || is.integer(port)) {
|
||||
if (!quiet) {
|
||||
message('\n', 'Listening on http://', host, ':', port)
|
||||
hostString <- host
|
||||
if (httpuv::ipFamily(host) == 6L)
|
||||
hostString <- paste0("[", hostString, "]")
|
||||
message('\n', 'Listening on http://', hostString, ':', port)
|
||||
}
|
||||
return(startServer(host, port, handlerManager$createHttpuvApp()))
|
||||
} else if (is.character(port)) {
|
||||
@@ -443,21 +443,20 @@ startApp <- function(appObj, port, host, quiet) {
|
||||
# Run an application that was created by \code{\link{startApp}}. This
|
||||
# function should normally be called in a \code{while(TRUE)} loop.
|
||||
serviceApp <- function() {
|
||||
if (timerCallbacks$executeElapsed()) {
|
||||
for (shinysession in appsByToken$values()) {
|
||||
shinysession$manageHiddenOutputs()
|
||||
}
|
||||
timerCallbacks$executeElapsed()
|
||||
|
||||
flushReact()
|
||||
flushAllSessions()
|
||||
}
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
|
||||
# If this R session is interactive, then call service() with a short timeout
|
||||
# to keep the session responsive to user input
|
||||
maxTimeout <- ifelse(interactive(), 100, 1000)
|
||||
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent()))
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
|
||||
service(timeout)
|
||||
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
}
|
||||
|
||||
.shinyServerMinVersion <- '0.3.4'
|
||||
@@ -580,12 +579,16 @@ runApp <- function(appDir=getwd(),
|
||||
.globals$running <- FALSE
|
||||
}, add = TRUE)
|
||||
|
||||
# Enable per-app Shiny options
|
||||
# Enable per-app Shiny options, for shinyOptions() and getShinyOption().
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit({
|
||||
.globals$options <- oldOptionSet
|
||||
},add = TRUE)
|
||||
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
@@ -595,6 +598,11 @@ runApp <- function(appDir=getwd(),
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache"))) {
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
}
|
||||
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# The lines below set some of the app's running options, which
|
||||
@@ -731,7 +739,8 @@ runApp <- function(appDir=getwd(),
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
|
||||
# https://github.com/rstudio/shiny/issues/1784
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -773,8 +782,17 @@ runApp <- function(appDir=getwd(),
|
||||
}, add = TRUE)
|
||||
|
||||
if (!is.character(port)) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- if (identical(host, "0.0.0.0")) "127.0.0.1" else host
|
||||
browseHost <- host
|
||||
if (identical(host, "0.0.0.0")) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- "127.0.0.1"
|
||||
} else if (identical(host, "::")) {
|
||||
browseHost <- "::1"
|
||||
}
|
||||
|
||||
if (httpuv::ipFamily(browseHost) == 6L) {
|
||||
browseHost <- paste0("[", browseHost, "]")
|
||||
}
|
||||
|
||||
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
||||
if (is.function(launch.browser))
|
||||
@@ -798,12 +816,8 @@ runApp <- function(appDir=getwd(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
# If any observers were created before runApp was called, this will make
|
||||
# sure they run once the app starts. (Issue #1013)
|
||||
scheduleFlush()
|
||||
|
||||
while (!.globals$stopped) {
|
||||
serviceApp()
|
||||
..stacktracefloor..(serviceApp())
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
})
|
||||
|
||||
668
R/shiny.R
668
R/shiny.R
@@ -310,7 +310,8 @@ workerId <- local({
|
||||
#' Similar to \code{sendCustomMessage}, but the message must be a raw vector
|
||||
#' and the registration method on the client is
|
||||
#' \code{Shiny.addBinaryMessageHandler(type, function(message){...})}. The
|
||||
#' message argument on the client will be a \href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
|
||||
#' message argument on the client will be a
|
||||
#' \href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
|
||||
#' }
|
||||
#' \item{sendInputMessage(inputId, message)}{
|
||||
#' Sends a message to an input on the session's client web page; if the input
|
||||
@@ -420,6 +421,7 @@ ShinySession <- R6Class(
|
||||
invalidatedOutputValues = 'Map',
|
||||
invalidatedOutputErrors = 'Map',
|
||||
inputMessageQueue = list(), # A list of inputMessages to send when flushed
|
||||
cycleStartActionQueue = list(), # A list of actions to perform to start a cycle
|
||||
.outputs = list(), # Keeps track of all the output observer objects
|
||||
.outputOptions = list(), # Options for each of the output observer objects
|
||||
progressKeys = 'character',
|
||||
@@ -438,10 +440,13 @@ ShinySession <- R6Class(
|
||||
restoredCallbacks = 'Callbacks',
|
||||
bookmarkExclude = character(0), # Names of inputs to exclude from bookmarking
|
||||
getBookmarkExcludeFuns = list(),
|
||||
timingRecorder = 'ShinyServerTimingRecorder',
|
||||
|
||||
testMode = FALSE, # Are we running in test mode?
|
||||
testExportExprs = list(),
|
||||
outputValues = list(), # Saved output values (for testing mode)
|
||||
currentOutputName = NULL, # Name of the currently-running output
|
||||
outputInfo = list(), # List of information for each output
|
||||
testSnapshotUrl = character(0),
|
||||
|
||||
sendResponse = function(requestMsg, value) {
|
||||
@@ -488,6 +493,16 @@ ShinySession <- R6Class(
|
||||
return(defaultValue)
|
||||
return(result)
|
||||
},
|
||||
withCurrentOutput = function(name, expr) {
|
||||
if (!is.null(private$currentOutputName)) {
|
||||
stop("Nested calls to withCurrentOutput() are not allowed.")
|
||||
}
|
||||
|
||||
promises::with_promise_domain(
|
||||
createVarPromiseDomain(private, "currentOutputName", name),
|
||||
expr
|
||||
)
|
||||
},
|
||||
shouldSuspend = function(name) {
|
||||
# Find corresponding hidden state clientData variable, with the format
|
||||
# "output_foo_hidden". (It comes from .clientdata_output_foo_hidden
|
||||
@@ -513,95 +528,6 @@ ShinySession <- R6Class(
|
||||
self$onSessionEnded(private$fileUploadContext$rmUploadDirs)
|
||||
},
|
||||
|
||||
createBookmarkObservers = function() {
|
||||
# This is to be called from the initialization. It registers observers
|
||||
# for bookmarking to work.
|
||||
|
||||
# Get bookmarking config
|
||||
store <- getShinyOption("bookmarkStore", default = "disable")
|
||||
if (store == "disable")
|
||||
return()
|
||||
|
||||
# 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")))
|
||||
{
|
||||
showNotification(
|
||||
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
|
||||
duration = NULL, type = "warning", session = self
|
||||
)
|
||||
return()
|
||||
}
|
||||
|
||||
withReactiveDomain(self, {
|
||||
# This observer fires when the bookmark button is clicked.
|
||||
observeEvent(self$input[["._bookmark_"]], {
|
||||
self$doBookmark()
|
||||
})
|
||||
|
||||
# If there was an error initializing the current restore context, show
|
||||
# notification in the client.
|
||||
observe({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (!is.null(rc$initErrorMessage)) {
|
||||
showNotification(
|
||||
paste("Error in RestoreContext initialization:", rc$initErrorMessage),
|
||||
duration = NULL, type = "error"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
# Run the onRestore function at the beginning of the flush cycle, but after
|
||||
# the server function has been executed.
|
||||
observe({
|
||||
if (private$restoreCallbacks$count() > 0) {
|
||||
tryCatch(
|
||||
withLogErrors(
|
||||
isolate({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (rc$active) {
|
||||
restoreState <- getCurrentRestoreContext()$asList()
|
||||
private$restoreCallbacks$invoke(restoreState)
|
||||
}
|
||||
})
|
||||
),
|
||||
error = function(e) {
|
||||
showNotification(
|
||||
paste0("Error calling onRestore callback: ", e$message),
|
||||
duration = NULL, type = "error"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}, priority = 1000000)
|
||||
|
||||
# Run the onRestored function after the flush cycle completes and information
|
||||
# is sent to the client.
|
||||
self$onFlushed(function() {
|
||||
if (private$restoredCallbacks$count() > 0) {
|
||||
|
||||
tryCatch(
|
||||
withLogErrors(
|
||||
isolate({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (rc$active) {
|
||||
restoreState <- getCurrentRestoreContext()$asList()
|
||||
private$restoredCallbacks$invoke(restoreState)
|
||||
}
|
||||
})
|
||||
),
|
||||
error = function(e) {
|
||||
msg <- paste0("Error calling onRestored callback: ", e$message)
|
||||
showNotification(msg, duration = NULL, type = "error")
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
}) # withReactiveDomain
|
||||
},
|
||||
|
||||
# Modules (scopes) call this to register a function that returns a vector
|
||||
# of names to exclude from bookmarking. The function should return
|
||||
# something like c("scope1-x", "scope1-y"). This doesn't use a Callback
|
||||
@@ -729,7 +655,7 @@ ShinySession <- R6Class(
|
||||
} else if (identical(format, "rds")) {
|
||||
tmpfile <- tempfile("shinytest", fileext = ".rds")
|
||||
saveRDS(values, tmpfile)
|
||||
on.exit(unlink(tmpfile))
|
||||
on.exit(unlink(tmpfile), add = TRUE)
|
||||
|
||||
content <- readBin(tmpfile, "raw", n = file.info(tmpfile)$size)
|
||||
httpResponse(200, "application/octet-stream", content)
|
||||
@@ -753,6 +679,15 @@ ShinySession <- R6Class(
|
||||
getSnapshotPreprocessInput = function(name) {
|
||||
fun <- private$.input$getMeta(name, "shiny.snapshot.preprocess")
|
||||
fun %OR% identity
|
||||
},
|
||||
|
||||
# See cycleStartAction
|
||||
startCycle = function() {
|
||||
if (length(private$cycleStartActionQueue) > 0) {
|
||||
head <- private$cycleStartActionQueue[[1L]]
|
||||
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
|
||||
head()
|
||||
}
|
||||
}
|
||||
),
|
||||
public = list(
|
||||
@@ -768,6 +703,7 @@ ShinySession <- R6Class(
|
||||
request = 'ANY', # Websocket request object
|
||||
singletons = character(0), # Tracks singleton HTML fragments sent to the page
|
||||
userData = 'environment',
|
||||
cache = NULL, # A cache object used in the session
|
||||
user = NULL,
|
||||
groups = NULL,
|
||||
|
||||
@@ -783,8 +719,9 @@ ShinySession <- R6Class(
|
||||
private$flushCallbacks <- Callbacks$new()
|
||||
private$flushedCallbacks <- Callbacks$new()
|
||||
private$inputReceivedCallbacks <- Callbacks$new()
|
||||
private$.input <- ReactiveValues$new()
|
||||
private$.clientData <- ReactiveValues$new()
|
||||
private$.input <- ReactiveValues$new(dedupe = FALSE)
|
||||
private$.clientData <- ReactiveValues$new(dedupe = TRUE)
|
||||
private$timingRecorder <- ShinyServerTimingRecorder$new()
|
||||
self$progressStack <- Stack$new()
|
||||
self$files <- Map$new()
|
||||
self$downloads <- Map$new()
|
||||
@@ -801,11 +738,12 @@ ShinySession <- R6Class(
|
||||
private$.outputs <- list()
|
||||
private$.outputOptions <- list()
|
||||
|
||||
self$cache <- MemoryCache$new()
|
||||
|
||||
private$bookmarkCallbacks <- Callbacks$new()
|
||||
private$bookmarkedCallbacks <- Callbacks$new()
|
||||
private$restoreCallbacks <- Callbacks$new()
|
||||
private$restoredCallbacks <- Callbacks$new()
|
||||
private$createBookmarkObservers()
|
||||
|
||||
private$testMode <- .globals$testMode
|
||||
private$enableTestSnapshot()
|
||||
@@ -833,6 +771,15 @@ ShinySession <- R6Class(
|
||||
)
|
||||
)
|
||||
},
|
||||
startTiming = function(guid) {
|
||||
if (!is.null(guid)) {
|
||||
private$timingRecorder$start(guid)
|
||||
self$onFlush(private$timingRecorder$stop)
|
||||
}
|
||||
},
|
||||
requestFlush = function() {
|
||||
appsNeedingFlush$set(self$token, self)
|
||||
},
|
||||
rootScope = function() {
|
||||
self
|
||||
},
|
||||
@@ -1029,8 +976,9 @@ ShinySession <- R6Class(
|
||||
stop("x must be a reactivevalues object")
|
||||
|
||||
impl <- .subset2(x, 'impl')
|
||||
impl$freeze(name)
|
||||
self$onFlushed(function() impl$thaw(name))
|
||||
key <- .subset2(x, 'ns')(name)
|
||||
impl$freeze(key)
|
||||
self$onFlushed(function() impl$thaw(key))
|
||||
},
|
||||
|
||||
onSessionEnded = function(sessionEndedCallback) {
|
||||
@@ -1064,8 +1012,6 @@ ShinySession <- R6Class(
|
||||
}
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
|
||||
flushReact()
|
||||
flushAllSessions()
|
||||
},
|
||||
isClosed = function() {
|
||||
return(self$closed)
|
||||
@@ -1094,9 +1040,16 @@ ShinySession <- R6Class(
|
||||
# name not working unless name was eagerly evaluated. Yikes!
|
||||
force(name)
|
||||
|
||||
# If overwriting an output object, suspend the previous copy of it
|
||||
# If overwriting an output object, destroy the previous copy of it
|
||||
if (!is.null(private$.outputs[[name]])) {
|
||||
private$.outputs[[name]]$suspend()
|
||||
private$.outputs[[name]]$destroy()
|
||||
}
|
||||
|
||||
if (is.null(func)) {
|
||||
# If func is null, give it an "empty" output function so it can go
|
||||
# through the logic below. If we simply returned at this point, the
|
||||
# previous output (if any) would continue to show in the client.
|
||||
func <- missingOutput
|
||||
}
|
||||
|
||||
if (is.function(func)) {
|
||||
@@ -1129,56 +1082,68 @@ ShinySession <- R6Class(
|
||||
name = name, status = 'recalculating'
|
||||
))
|
||||
|
||||
value <- tryCatch(
|
||||
shinyCallingHandlers(func()),
|
||||
shiny.custom.error = function(cond) {
|
||||
if (isTRUE(getOption("show.error.messages"))) printError(cond)
|
||||
structure(list(), class = "try-error", condition = cond)
|
||||
},
|
||||
shiny.output.cancel = function(cond) {
|
||||
structure(list(), class = "cancel-output")
|
||||
},
|
||||
shiny.silent.error = function(cond) {
|
||||
# Don't let shiny.silent.error go through the normal stop
|
||||
# path of try, because we don't want it to print. But we
|
||||
# do want to try to return the same looking result so that
|
||||
# the code below can send the error to the browser.
|
||||
structure(list(), class = "try-error", condition = cond)
|
||||
},
|
||||
error = function(cond) {
|
||||
if (isTRUE(getOption("show.error.messages"))) printError(cond)
|
||||
if (getOption("shiny.sanitize.errors", FALSE)) {
|
||||
cond <- simpleError(paste("An error has occurred. Check your",
|
||||
"logs or contact the app author for",
|
||||
"clarification."))
|
||||
# This shinyCallingHandlers should maybe be at a higher level,
|
||||
# to include the $then/$catch calls below?
|
||||
hybrid_chain(
|
||||
hybrid_chain(
|
||||
{
|
||||
private$withCurrentOutput(name, {
|
||||
shinyCallingHandlers(func())
|
||||
})
|
||||
},
|
||||
catch = function(cond) {
|
||||
if (inherits(cond, "shiny.custom.error")) {
|
||||
if (isTRUE(getOption("show.error.messages"))) printError(cond)
|
||||
structure(list(), class = "try-error", condition = cond)
|
||||
} else if (inherits(cond, "shiny.output.cancel")) {
|
||||
structure(list(), class = "cancel-output")
|
||||
} else if (inherits(cond, "shiny.silent.error")) {
|
||||
# Don't let shiny.silent.error go through the normal stop
|
||||
# path of try, because we don't want it to print. But we
|
||||
# do want to try to return the same looking result so that
|
||||
# the code below can send the error to the browser.
|
||||
structure(list(), class = "try-error", condition = cond)
|
||||
} else {
|
||||
if (isTRUE(getOption("show.error.messages"))) printError(cond)
|
||||
if (getOption("shiny.sanitize.errors", FALSE)) {
|
||||
cond <- simpleError(paste("An error has occurred. Check your",
|
||||
"logs or contact the app author for",
|
||||
"clarification."))
|
||||
}
|
||||
invisible(structure(list(), class = "try-error", condition = cond))
|
||||
}
|
||||
}
|
||||
invisible(structure(list(), class = "try-error", condition = cond))
|
||||
},
|
||||
finally = {
|
||||
),
|
||||
function(value) {
|
||||
# Needed so that Shiny knows to flush the outputs. Even if no
|
||||
# outputs/errors are queued, it's necessary to flush so that the
|
||||
# client knows that progress is over.
|
||||
self$requestFlush()
|
||||
|
||||
private$sendMessage(recalculating = list(
|
||||
name = name, status = 'recalculated'
|
||||
))
|
||||
|
||||
if (inherits(value, "cancel-output")) {
|
||||
return()
|
||||
}
|
||||
|
||||
private$invalidatedOutputErrors$remove(name)
|
||||
private$invalidatedOutputValues$remove(name)
|
||||
|
||||
if (inherits(value, 'try-error')) {
|
||||
cond <- attr(value, 'condition')
|
||||
type <- setdiff(class(cond), c('simpleError', 'error', 'condition'))
|
||||
private$invalidatedOutputErrors$set(
|
||||
name,
|
||||
list(message = cond$message,
|
||||
call = utils::capture.output(print(cond$call)),
|
||||
type = if (length(type)) type))
|
||||
}
|
||||
else
|
||||
private$invalidatedOutputValues$set(name, value)
|
||||
}
|
||||
)
|
||||
|
||||
if (inherits(value, "cancel-output")) {
|
||||
return()
|
||||
}
|
||||
|
||||
private$invalidatedOutputErrors$remove(name)
|
||||
private$invalidatedOutputValues$remove(name)
|
||||
|
||||
if (inherits(value, 'try-error')) {
|
||||
cond <- attr(value, 'condition')
|
||||
type <- setdiff(class(cond), c('simpleError', 'error', 'condition'))
|
||||
private$invalidatedOutputErrors$set(
|
||||
name,
|
||||
list(message = cond$message,
|
||||
call = utils::capture.output(print(cond$call)),
|
||||
type = if (length(type)) type))
|
||||
}
|
||||
else
|
||||
private$invalidatedOutputValues$set(name, value)
|
||||
}, suspended=private$shouldSuspend(name), label=label)
|
||||
|
||||
# If any output attributes were added to the render function attach
|
||||
@@ -1200,6 +1165,11 @@ ShinySession <- R6Class(
|
||||
}
|
||||
},
|
||||
flushOutput = function() {
|
||||
if (private$busyCount > 0)
|
||||
return()
|
||||
|
||||
appsNeedingFlush$remove(self$token)
|
||||
|
||||
if (self$isClosed())
|
||||
return()
|
||||
|
||||
@@ -1217,49 +1187,59 @@ ShinySession <- R6Class(
|
||||
)
|
||||
}
|
||||
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
private$flushCallbacks$invoke(..stacktraceon = TRUE)
|
||||
|
||||
# Schedule execution of onFlushed callbacks
|
||||
on.exit({
|
||||
withReactiveDomain(self, {
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
private$flushedCallbacks$invoke(..stacktraceon = TRUE)
|
||||
private$flushCallbacks$invoke(..stacktraceon = TRUE)
|
||||
|
||||
# If one of the flushedCallbacks added anything to send to the client,
|
||||
# or invalidated any observers, set up another flush cycle.
|
||||
if (hasPendingUpdates() || .getReactiveEnvironment()$hasPendingFlush()) {
|
||||
scheduleFlush()
|
||||
# Schedule execution of onFlushed callbacks
|
||||
on.exit({
|
||||
withReactiveDomain(self, {
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
private$flushedCallbacks$invoke(..stacktraceon = TRUE)
|
||||
})
|
||||
}, add = TRUE)
|
||||
|
||||
if (!hasPendingUpdates()) {
|
||||
# Normally, if there are no updates, simply return without sending
|
||||
# anything to the client. But if we are in test mode, we still want to
|
||||
# send a message with blank `values`, so that the client knows that
|
||||
# any changed inputs have been received by the server and processed.
|
||||
if (isTRUE(private$testMode)) {
|
||||
private$sendMessage( values = list() )
|
||||
}
|
||||
return(invisible())
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasPendingUpdates()) {
|
||||
# Normally, if there are no updates, simply return without sending
|
||||
# anything to the client. But if we are in test mode, we still want to
|
||||
# send a message with blank `values`, so that the client knows that
|
||||
# any changed inputs have been received by the server and processed.
|
||||
private$progressKeys <- character(0)
|
||||
values <- as.list(private$invalidatedOutputValues)
|
||||
private$invalidatedOutputValues <- Map$new()
|
||||
errors <- as.list(private$invalidatedOutputErrors)
|
||||
private$invalidatedOutputErrors <- Map$new()
|
||||
inputMessages <- private$inputMessageQueue
|
||||
private$inputMessageQueue <- list()
|
||||
|
||||
if (isTRUE(private$testMode)) {
|
||||
private$sendMessage( values = list() )
|
||||
private$storeOutputValues(mergeVectors(values, errors))
|
||||
}
|
||||
return(invisible())
|
||||
|
||||
private$sendMessage(
|
||||
errors = errors,
|
||||
values = values,
|
||||
inputMessages = inputMessages
|
||||
)
|
||||
})
|
||||
},
|
||||
# Schedule an action to execute not (necessarily) now, but when no observers
|
||||
# that belong to this session are busy executing. This helps prevent (but
|
||||
# does not guarantee) inputs and reactive values from changing underneath
|
||||
# async observers as they run.
|
||||
cycleStartAction = function(callback) {
|
||||
private$cycleStartActionQueue <- c(private$cycleStartActionQueue, list(callback))
|
||||
# If no observers are running in this session, we're safe to proceed.
|
||||
# Otherwise, startCycle() will be called later, via decrementBusyCount().
|
||||
if (private$busyCount == 0L) {
|
||||
private$startCycle()
|
||||
}
|
||||
|
||||
private$progressKeys <- character(0)
|
||||
values <- as.list(private$invalidatedOutputValues)
|
||||
private$invalidatedOutputValues <- Map$new()
|
||||
errors <- as.list(private$invalidatedOutputErrors)
|
||||
private$invalidatedOutputErrors <- Map$new()
|
||||
inputMessages <- private$inputMessageQueue
|
||||
private$inputMessageQueue <- list()
|
||||
|
||||
if (isTRUE(private$testMode)) {
|
||||
private$storeOutputValues(mergeVectors(values, errors))
|
||||
}
|
||||
|
||||
private$sendMessage(
|
||||
errors = errors,
|
||||
values = values,
|
||||
inputMessages = inputMessages
|
||||
)
|
||||
},
|
||||
showProgress = function(id) {
|
||||
'Send a message to the client that recalculation of the output identified
|
||||
@@ -1327,6 +1307,8 @@ ShinySession <- R6Class(
|
||||
|
||||
# Add to input message queue
|
||||
private$inputMessageQueue[[length(private$inputMessageQueue) + 1]] <- data
|
||||
# Needed so that Shiny knows to actually flush the input message queue
|
||||
self$requestFlush()
|
||||
},
|
||||
onFlush = function(flushCallback, once = TRUE) {
|
||||
if (!isTRUE(once)) {
|
||||
@@ -1351,6 +1333,135 @@ ShinySession <- R6Class(
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentOutputInfo = function() {
|
||||
name <- private$currentOutputName
|
||||
|
||||
tmp_info <- private$outputInfo[[name]] %OR% list(name = name)
|
||||
|
||||
# cd_names() returns names of all items in clientData, without taking a
|
||||
# reactive dependency. It is a function and it's memoized, so that we do
|
||||
# the (relatively) expensive isolate(names(...)) call only when needed,
|
||||
# and at most one time in this function.
|
||||
.cd_names <- NULL
|
||||
cd_names <- function() {
|
||||
if (is.null(.cd_names)) {
|
||||
.cd_names <<- isolate(names(self$clientData))
|
||||
}
|
||||
.cd_names
|
||||
}
|
||||
|
||||
# If we don't already have width for this output info, see if it's
|
||||
# present, and if so, add it.
|
||||
if (! ("width" %in% names(tmp_info)) ) {
|
||||
width_name <- paste0("output_", name, "_width")
|
||||
if (width_name %in% cd_names()) {
|
||||
tmp_info$width <- reactive({
|
||||
self$clientData[[width_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (! ("height" %in% names(tmp_info)) ) {
|
||||
height_name <- paste0("output_", name, "_height")
|
||||
if (height_name %in% cd_names()) {
|
||||
tmp_info$height <- reactive({
|
||||
self$clientData[[height_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private$outputInfo[[name]] <- tmp_info
|
||||
private$outputInfo[[name]]
|
||||
},
|
||||
|
||||
createBookmarkObservers = function() {
|
||||
# This registers observers for bookmarking to work.
|
||||
|
||||
# Get bookmarking config
|
||||
store <- getShinyOption("bookmarkStore", default = "disable")
|
||||
if (store == "disable")
|
||||
return()
|
||||
|
||||
# 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")))
|
||||
{
|
||||
showNotification(
|
||||
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
|
||||
duration = NULL, type = "warning", session = self
|
||||
)
|
||||
return()
|
||||
}
|
||||
|
||||
withReactiveDomain(self, {
|
||||
# This observer fires when the bookmark button is clicked.
|
||||
observeEvent(self$input[["._bookmark_"]], {
|
||||
self$doBookmark()
|
||||
})
|
||||
|
||||
# If there was an error initializing the current restore context, show
|
||||
# notification in the client.
|
||||
observe({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (!is.null(rc$initErrorMessage)) {
|
||||
showNotification(
|
||||
paste("Error in RestoreContext initialization:", rc$initErrorMessage),
|
||||
duration = NULL, type = "error"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
# Run the onRestore function at the beginning of the flush cycle, but after
|
||||
# the server function has been executed.
|
||||
observe({
|
||||
if (private$restoreCallbacks$count() > 0) {
|
||||
tryCatch(
|
||||
withLogErrors(
|
||||
isolate({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (rc$active) {
|
||||
restoreState <- getCurrentRestoreContext()$asList()
|
||||
private$restoreCallbacks$invoke(restoreState)
|
||||
}
|
||||
})
|
||||
),
|
||||
error = function(e) {
|
||||
showNotification(
|
||||
paste0("Error calling onRestore callback: ", e$message),
|
||||
duration = NULL, type = "error"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}, priority = 1000000)
|
||||
|
||||
# Run the onRestored function after the flush cycle completes and information
|
||||
# is sent to the client.
|
||||
self$onFlushed(function() {
|
||||
if (private$restoredCallbacks$count() > 0) {
|
||||
|
||||
tryCatch(
|
||||
withLogErrors(
|
||||
isolate({
|
||||
rc <- getCurrentRestoreContext()
|
||||
if (rc$active) {
|
||||
restoreState <- getCurrentRestoreContext()$asList()
|
||||
private$restoredCallbacks$invoke(restoreState)
|
||||
}
|
||||
})
|
||||
),
|
||||
error = function(e) {
|
||||
msg <- paste0("Error calling onRestored callback: ", e$message)
|
||||
showNotification(msg, duration = NULL, type = "error")
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
}) # withReactiveDomain
|
||||
},
|
||||
|
||||
setBookmarkExclude = function(names) {
|
||||
private$bookmarkExclude <- names
|
||||
},
|
||||
@@ -1697,32 +1808,44 @@ ShinySession <- R6Class(
|
||||
if (nzchar(ext))
|
||||
ext <- paste(".", ext, sep = "")
|
||||
tmpdata <- tempfile(fileext = ext)
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
result <- try(shinyCallingHandlers(Context$new(getDefaultReactiveDomain(), '[download]')$run(
|
||||
function() { ..stacktraceon..(download$func(tmpdata)) }
|
||||
)), silent = TRUE)
|
||||
if (inherits(result, 'try-error')) {
|
||||
unlink(tmpdata)
|
||||
stop(attr(result, "condition", exact = TRUE))
|
||||
}
|
||||
if (!file.exists(tmpdata)) {
|
||||
# If no file was created, return a 404
|
||||
return(httpResponse(404, content = "404 Not found"))
|
||||
}
|
||||
return(httpResponse(
|
||||
200,
|
||||
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] == '',
|
||||
'attachment; filename="' %.%
|
||||
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
|
||||
'"',
|
||||
'attachment'
|
||||
),
|
||||
'Cache-Control'='no-cache')))
|
||||
return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() {
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
promises::with_promise_domain(createStackTracePromiseDomain(), {
|
||||
self$incrementBusyCount()
|
||||
hybrid_chain(
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
try(..stacktraceon..(download$func(tmpdata)), silent = TRUE),
|
||||
function(result) {
|
||||
if (inherits(result, 'try-error')) {
|
||||
unlink(tmpdata)
|
||||
stop(attr(result, "condition", exact = TRUE))
|
||||
}
|
||||
if (!file.exists(tmpdata)) {
|
||||
# If no file was created, return a 404
|
||||
return(httpResponse(404, content = "404 Not found"))
|
||||
}
|
||||
return(httpResponse(
|
||||
200,
|
||||
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] == '',
|
||||
'attachment; filename="' %.%
|
||||
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
|
||||
'"',
|
||||
'attachment'
|
||||
),
|
||||
'Cache-Control'='no-cache')))
|
||||
},
|
||||
finally = function() {
|
||||
self$decrementBusyCount()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
if (matches[2] == 'dataobj') {
|
||||
@@ -1787,9 +1910,13 @@ ShinySession <- R6Class(
|
||||
},
|
||||
# This function suspends observers for hidden outputs and resumes observers
|
||||
# for un-hidden outputs.
|
||||
manageHiddenOutputs = function() {
|
||||
manageHiddenOutputs = function(outputsToCheck = NULL) {
|
||||
if (is.null(outputsToCheck)) {
|
||||
outputsToCheck <- names(private$.outputs)
|
||||
}
|
||||
|
||||
# Find hidden state for each output, and suspend/resume accordingly
|
||||
for (outputName in names(private$.outputs)) {
|
||||
for (outputName in outputsToCheck) {
|
||||
if (private$shouldSuspend(outputName)) {
|
||||
private$.outputs[[outputName]]$suspend()
|
||||
} else {
|
||||
@@ -1797,24 +1924,39 @@ ShinySession <- R6Class(
|
||||
}
|
||||
}
|
||||
},
|
||||
# Set the normal and client data input variables
|
||||
manageInputs = function(data) {
|
||||
# Set the normal and client data input variables. Normally, managing
|
||||
# inputs doesn't take immediate effect when there are observers that
|
||||
# are pending execution or currently executing (including having
|
||||
# started async operations that have yielded control, but not yet
|
||||
# completed). The `now` argument can force this. It should generally
|
||||
# not be used, but we're adding it to get around a show-stopping bug
|
||||
# for Shiny v1.1 (see the call site for more details).
|
||||
manageInputs = function(data, now = FALSE) {
|
||||
force(data)
|
||||
doManageInputs <- function() {
|
||||
private$inputReceivedCallbacks$invoke(data)
|
||||
|
||||
private$inputReceivedCallbacks$invoke(data)
|
||||
data_names <- names(data)
|
||||
|
||||
data_names <- names(data)
|
||||
# Separate normal input variables from client data input variables
|
||||
clientdata_idx <- grepl("^.clientdata_", data_names)
|
||||
|
||||
# Separate normal input variables from client data input variables
|
||||
clientdata_idx <- grepl("^.clientdata_", data_names)
|
||||
# Set normal (non-clientData) input values
|
||||
private$.input$mset(data[data_names[!clientdata_idx]])
|
||||
|
||||
# Set normal (non-clientData) input values
|
||||
private$.input$mset(data[data_names[!clientdata_idx]])
|
||||
# Strip off .clientdata_ from clientdata input names, and set values
|
||||
input_clientdata <- data[data_names[clientdata_idx]]
|
||||
names(input_clientdata) <- sub("^.clientdata_", "",
|
||||
names(input_clientdata))
|
||||
private$.clientData$mset(input_clientdata)
|
||||
|
||||
# Strip off .clientdata_ from clientdata input names, and set values
|
||||
input_clientdata <- data[data_names[clientdata_idx]]
|
||||
names(input_clientdata) <- sub("^.clientdata_", "",
|
||||
names(input_clientdata))
|
||||
private$.clientData$mset(input_clientdata)
|
||||
self$manageHiddenOutputs()
|
||||
}
|
||||
if (isTRUE(now)) {
|
||||
doManageInputs()
|
||||
} else {
|
||||
self$cycleStartAction(doManageInputs)
|
||||
}
|
||||
},
|
||||
outputOptions = function(name, ...) {
|
||||
# If no name supplied, return the list of options for all outputs
|
||||
@@ -1839,7 +1981,7 @@ ShinySession <- R6Class(
|
||||
|
||||
# If any changes to suspendWhenHidden, need to re-run manageHiddenOutputs
|
||||
if ("suspendWhenHidden" %in% names(opts)) {
|
||||
self$manageHiddenOutputs()
|
||||
self$manageHiddenOutputs(name)
|
||||
}
|
||||
|
||||
if ("priority" %in% names(opts)) {
|
||||
@@ -1858,6 +2000,19 @@ ShinySession <- R6Class(
|
||||
private$busyCount <- private$busyCount - 1L
|
||||
if (private$busyCount == 0L) {
|
||||
private$sendMessage(busy = "idle")
|
||||
self$requestFlush()
|
||||
# We defer the call to startCycle() using later(), to defend against
|
||||
# cycles where we continually call startCycle which causes an observer
|
||||
# to fire which calls startCycle which causes an observer to fire...
|
||||
#
|
||||
# It's OK for these cycles to occur, but we must return control to the
|
||||
# event loop between iterations (or at least sometimes) in order to not
|
||||
# make the whole Shiny app go unresponsive.
|
||||
later::later(function() {
|
||||
if (private$busyCount == 0L) {
|
||||
private$startCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
),
|
||||
@@ -1962,6 +2117,16 @@ outputOptions <- function(x, name, ...) {
|
||||
.subset2(x, 'impl')$outputOptions(name, ...)
|
||||
}
|
||||
|
||||
|
||||
#' Get information about the output that is currently being executed.
|
||||
#'
|
||||
#' @param session The current Shiny session.
|
||||
#'
|
||||
#' @export
|
||||
getCurrentOutputInfo <- function(session = getDefaultReactiveDomain()) {
|
||||
session$getCurrentOutputInfo()
|
||||
}
|
||||
|
||||
#' Add callbacks for Shiny session events
|
||||
#'
|
||||
#' These functions are for registering callbacks on Shiny session events.
|
||||
@@ -2002,12 +2167,8 @@ onSessionEnded <- function(fun, session = getDefaultReactiveDomain()) {
|
||||
}
|
||||
|
||||
|
||||
scheduleFlush <- function() {
|
||||
timerCallbacks$schedule(0, function() {})
|
||||
}
|
||||
|
||||
flushAllSessions <- function() {
|
||||
lapply(appsByToken$values(), function(shinysession) {
|
||||
flushPendingSessions <- function() {
|
||||
lapply(appsNeedingFlush$values(), function(shinysession) {
|
||||
tryCatch(
|
||||
shinysession$flushOutput(),
|
||||
|
||||
@@ -2034,7 +2195,9 @@ flushAllSessions <- function() {
|
||||
#' called from within the server function, this will default to the current
|
||||
#' session, and the callback will be invoked when the current session ends. If
|
||||
#' \code{onStop} is called outside a server function, then the callback will
|
||||
#' be invoked with the application exits.
|
||||
#' be invoked with the application exits. If \code{NULL}, it is the same as
|
||||
#' calling \code{onStop} outside of the server function, and the callback will
|
||||
#' be invoked when the application exits.
|
||||
#'
|
||||
#'
|
||||
#' @seealso \code{\link{onSessionEnded}()} for the same functionality, but at
|
||||
@@ -2094,7 +2257,7 @@ flushAllSessions <- function() {
|
||||
#' }
|
||||
#' @export
|
||||
onStop <- function(fun, session = getDefaultReactiveDomain()) {
|
||||
if (is.null(getDefaultReactiveDomain())) {
|
||||
if (is.null(session)) {
|
||||
return(.globals$onStopCallbacks$register(fun))
|
||||
} else {
|
||||
# Note: In the future if we allow scoping the onStop() callback to modules
|
||||
@@ -2103,3 +2266,48 @@ onStop <- function(fun, session = getDefaultReactiveDomain()) {
|
||||
return(session$onSessionEnded(fun))
|
||||
}
|
||||
}
|
||||
|
||||
# Helper class for emitting log messages to stdout that will be interpreted by
|
||||
# a Shiny Server parent process. The duration it's trying to record is the time
|
||||
# between a websocket message being received, and the next flush to the client.
|
||||
ShinyServerTimingRecorder <- R6Class("ShinyServerTimingRecorder",
|
||||
cloneable = FALSE,
|
||||
public = list(
|
||||
initialize = function() {
|
||||
private$shiny_stdout <- if (exists(".shiny__stdout", globalenv()))
|
||||
get(".shiny__stdout", globalenv())
|
||||
else
|
||||
NULL
|
||||
private$guid <- NULL
|
||||
},
|
||||
start = function(guid) {
|
||||
if (is.null(private$shiny_stdout)) return()
|
||||
|
||||
private$guid <- guid
|
||||
if (!is.null(guid)) {
|
||||
private$write("n")
|
||||
}
|
||||
},
|
||||
stop = function() {
|
||||
if (is.null(private$shiny_stdout)) return()
|
||||
|
||||
if (!is.null(private$guid)) {
|
||||
private$write("x")
|
||||
private$guid <- NULL
|
||||
}
|
||||
}
|
||||
),
|
||||
private = list(
|
||||
shiny_stdout = NULL,
|
||||
guid = character(),
|
||||
write = function(code) {
|
||||
# eNter or eXit a flushReact
|
||||
writeLines(paste("_", code, "_flushReact ", private$guid,
|
||||
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
||||
sep=""), con=private$shiny_stdout)
|
||||
flush(private$shiny_stdout)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
missingOutput <- function(...) req(FALSE)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
globalVariables('func')
|
||||
utils::globalVariables('func')
|
||||
|
||||
#' Mark a function as a render function
|
||||
#'
|
||||
@@ -52,6 +52,49 @@ markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
|
||||
hasExecuted = hasExecuted)
|
||||
}
|
||||
|
||||
#' Implement render functions
|
||||
#'
|
||||
#' @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.
|
||||
#' @param transform A function that takes four arguments: \code{value},
|
||||
#' \code{session}, \code{name}, and \code{...} (for future-proofing). This
|
||||
#' function will be invoked each time a value is returned from \code{func},
|
||||
#' and is responsible for changing the value into a JSON-ready value to be
|
||||
#' JSON-encoded and sent to the browser.
|
||||
#' @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.
|
||||
#' @param 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).
|
||||
#' @return An annotated render function, ready to be assigned to an
|
||||
#' \code{output} slot.
|
||||
#'
|
||||
#' @export
|
||||
createRenderFunction <- function(
|
||||
func, transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL, outputArgs = NULL
|
||||
) {
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value, .visible) {
|
||||
transform(setVisible(value, .visible), shinysession, name, ...)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(outputFunc))
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
else
|
||||
renderFunc
|
||||
}
|
||||
|
||||
useRenderFunction <- function(renderFunc, inline = FALSE) {
|
||||
outputFunction <- attr(renderFunc, "outputFunc")
|
||||
outputArgs <- attr(renderFunc, "outputArgs")
|
||||
@@ -68,12 +111,16 @@ useRenderFunction <- function(renderFunc, inline = FALSE) {
|
||||
}
|
||||
|
||||
id <- createUniqueId(8, "out")
|
||||
# Make the id the first positional argument
|
||||
outputArgs <- c(list(id), outputArgs)
|
||||
|
||||
o <- getDefaultReactiveDomain()$output
|
||||
if (!is.null(o))
|
||||
if (!is.null(o)) {
|
||||
o[[id]] <- renderFunc
|
||||
# If there's a namespace, we must respect it
|
||||
id <- getDefaultReactiveDomain()$ns(id)
|
||||
}
|
||||
|
||||
# Make the id the first positional argument
|
||||
outputArgs <- c(list(id), outputArgs)
|
||||
|
||||
if (is.logical(formals(outputFunction)[["inline"]]) && !("inline" %in% names(outputArgs))) {
|
||||
outputArgs[["inline"]] <- inline
|
||||
@@ -222,26 +269,25 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
deleteFile=TRUE, outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
imageinfo <- func()
|
||||
# Should the file be deleted after being sent? If .deleteFile not set or if
|
||||
# TRUE, then delete; otherwise don't delete.
|
||||
if (deleteFile) {
|
||||
on.exit(unlink(imageinfo$src))
|
||||
}
|
||||
createRenderFunction(func,
|
||||
transform = function(imageinfo, session, name, ...) {
|
||||
# Should the file be deleted after being sent? If .deleteFile not set or if
|
||||
# TRUE, then delete; otherwise don't delete.
|
||||
if (deleteFile) {
|
||||
on.exit(unlink(imageinfo$src))
|
||||
}
|
||||
|
||||
# If contentType not specified, autodetect based on extension
|
||||
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
|
||||
# If contentType not specified, autodetect based on extension
|
||||
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')]
|
||||
# Extra values are everything in imageinfo except 'src' and 'contentType'
|
||||
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
|
||||
|
||||
# Return a list with src, and other img attributes
|
||||
c(src = shinysession$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
}
|
||||
|
||||
markRenderFunction(imageOutput, renderFunc, outputArgs = outputArgs)
|
||||
# Return a list with src, and other img attributes
|
||||
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
},
|
||||
imageOutput, outputArgs)
|
||||
}
|
||||
|
||||
|
||||
@@ -281,15 +327,74 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
width = getOption('width'), outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
# Set a promise domain that sets the console width
|
||||
# and captures output
|
||||
# op <- options(width = width)
|
||||
# on.exit(options(op), add = TRUE)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
paste(utils::capture.output(func()), collapse = "\n")
|
||||
domain <- createRenderPrintPromiseDomain(width)
|
||||
hybrid_chain(
|
||||
{
|
||||
promises::with_promise_domain(domain, func())
|
||||
},
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
|
||||
}
|
||||
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
res
|
||||
},
|
||||
finally = function() {
|
||||
close(domain$conn)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
|
||||
}
|
||||
|
||||
createRenderPrintPromiseDomain <- function(width) {
|
||||
f <- file()
|
||||
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
function(...) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
function(...) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
op <- options(width = width)
|
||||
on.exit(options(op), add = TRUE)
|
||||
|
||||
sink(f, append = TRUE)
|
||||
on.exit(sink(NULL), add = TRUE)
|
||||
|
||||
force(expr)
|
||||
},
|
||||
conn = f
|
||||
)
|
||||
}
|
||||
|
||||
#' Text Output
|
||||
#'
|
||||
#' Makes a reactive version of the given function that also uses
|
||||
@@ -321,18 +426,18 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
value <- func()
|
||||
return(paste(utils::capture.output(cat(value)), collapse="\n"))
|
||||
}
|
||||
|
||||
markRenderFunction(textOutput, renderFunc, outputArgs = outputArgs)
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(value, session, name, ...) {
|
||||
paste(utils::capture.output(cat(value)), collapse="\n")
|
||||
},
|
||||
textOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
#' UI Output
|
||||
#'
|
||||
#' \bold{Experimental feature.} Makes a reactive version of a function that
|
||||
#' generates HTML using the Shiny UI library.
|
||||
#' Renders reactive HTML using the Shiny UI library.
|
||||
#'
|
||||
#' The corresponding HTML output tag should be \code{div} and have the CSS class
|
||||
#' name \code{shiny-html-output} (or use \code{\link{uiOutput}}).
|
||||
@@ -346,7 +451,7 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
#' call to \code{\link{uiOutput}} when \code{renderUI} is used in an
|
||||
#' interactive R Markdown document.
|
||||
#'
|
||||
#' @seealso conditionalPanel
|
||||
#' @seealso \code{\link{uiOutput}}
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
@@ -371,15 +476,16 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
result <- func()
|
||||
if (is.null(result) || length(result) == 0)
|
||||
return(NULL)
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(result, shinysession, name, ...) {
|
||||
if (is.null(result) || length(result) == 0)
|
||||
return(NULL)
|
||||
|
||||
processDeps(result, shinysession)
|
||||
}
|
||||
|
||||
markRenderFunction(uiOutput, renderFunc, outputArgs = outputArgs)
|
||||
processDeps(result, shinysession)
|
||||
},
|
||||
uiOutput, outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
#' File Downloads
|
||||
@@ -517,27 +623,31 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
if (is.function(options)) options <- options()
|
||||
options <- checkDT9(options)
|
||||
res <- checkAsIs(options)
|
||||
data <- func()
|
||||
if (length(dim(data)) != 2) return() # expects a rectangular data object
|
||||
if (is.data.frame(data)) data <- as.data.frame(data)
|
||||
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
|
||||
colnames <- colnames(data)
|
||||
# if escape is column names, turn names to numeric indices
|
||||
if (is.character(escape)) {
|
||||
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
|
||||
if (any(is.na(escape)))
|
||||
stop("Some column names in the 'escape' argument not found in data")
|
||||
}
|
||||
colnames[escape] <- htmlEscape(colnames[escape])
|
||||
if (!is.logical(escape)) {
|
||||
if (!is.numeric(escape))
|
||||
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
|
||||
escape <- paste(escape, collapse = ',')
|
||||
}
|
||||
list(
|
||||
colnames = colnames, action = action, options = res$options,
|
||||
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
|
||||
callback = paste(callback, collapse = '\n'), escape = escape
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(data) {
|
||||
if (length(dim(data)) != 2) return() # expects a rectangular data object
|
||||
if (is.data.frame(data)) data <- as.data.frame(data)
|
||||
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
|
||||
colnames <- colnames(data)
|
||||
# if escape is column names, turn names to numeric indices
|
||||
if (is.character(escape)) {
|
||||
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
|
||||
if (any(is.na(escape)))
|
||||
stop("Some column names in the 'escape' argument not found in data")
|
||||
}
|
||||
colnames[escape] <- htmlEscape(colnames[escape])
|
||||
if (!is.logical(escape)) {
|
||||
if (!is.numeric(escape))
|
||||
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
|
||||
escape <- paste(escape, collapse = ',')
|
||||
}
|
||||
list(
|
||||
colnames = colnames, action = action, options = res$options,
|
||||
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
|
||||
callback = paste(callback, collapse = '\n'), escape = escape
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
19
R/timer.R
19
R/timer.R
@@ -42,6 +42,17 @@ TimerCallbacks <- R6Class(
|
||||
|
||||
return(id)
|
||||
},
|
||||
unschedule = function(id) {
|
||||
toRemoveIndices <- .times$id %in% id
|
||||
toRemoveIds <- .times[toRemoveIndices, "id", drop = TRUE]
|
||||
if (length(toRemoveIds) > 0) {
|
||||
.times <<- .times[!toRemoveIndices,]
|
||||
for (toRemoveId in as.character(toRemoveIds)) {
|
||||
.funcs$remove(toRemoveId)
|
||||
}
|
||||
}
|
||||
return(id %in% toRemoveIds)
|
||||
},
|
||||
timeToNextEvent = function() {
|
||||
if (dim(.times)[1] == 0)
|
||||
return(Inf)
|
||||
@@ -79,13 +90,9 @@ timerCallbacks <- TimerCallbacks$new()
|
||||
|
||||
scheduleTask <- function(millis, callback) {
|
||||
cancelled <- FALSE
|
||||
timerCallbacks$schedule(millis, function() {
|
||||
if (!cancelled)
|
||||
callback()
|
||||
})
|
||||
id <- timerCallbacks$schedule(millis, callback)
|
||||
|
||||
function() {
|
||||
cancelled <<- TRUE
|
||||
callback <<- NULL # to allow for callback to be gc'ed
|
||||
invisible(timerCallbacks$unschedule(id))
|
||||
}
|
||||
}
|
||||
|
||||
182
R/update-input.R
182
R/update-input.R
@@ -383,13 +383,17 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
#' Change the value of a slider input on the client
|
||||
#' Update Slider Input Widget
|
||||
#'
|
||||
#' Change the value of a slider input on the client.
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param min Minimum value.
|
||||
#' @param max Maximum value.
|
||||
#' @param step Step size.
|
||||
#' @param timeFormat Date and POSIXt formatting.
|
||||
#' @param timezone The timezone offset for POSIXt objects.
|
||||
#'
|
||||
#' @seealso \code{\link{sliderInput}}
|
||||
#'
|
||||
@@ -422,22 +426,15 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' }
|
||||
#' @export
|
||||
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL)
|
||||
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
|
||||
{
|
||||
# Make sure that value, min, max all have the same type, because we need
|
||||
# special handling for dates and datetimes.
|
||||
vals <- dropNulls(list(value, min, max))
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
type <- unique(lapply(vals, function(x) {
|
||||
if (inherits(x, "Date")) "date"
|
||||
else if (inherits(x, "POSIXt")) "datetime"
|
||||
else "number"
|
||||
}))
|
||||
if (length(type) > 1) {
|
||||
stop("Type mismatch for value, min, and max")
|
||||
if (is.null(timeFormat)) {
|
||||
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
|
||||
}
|
||||
|
||||
if ((length(type) == 1) && (type == "date" || type == "datetime")) {
|
||||
if (dataType == "date" || dataType == "datetime") {
|
||||
to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
|
||||
if (!is.null(min)) min <- to_ms(min)
|
||||
if (!is.null(max)) max <- to_ms(max)
|
||||
@@ -449,7 +446,10 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
value = formatNoSci(value),
|
||||
min = formatNoSci(min),
|
||||
max = formatNoSci(max),
|
||||
step = formatNoSci(step)
|
||||
step = formatNoSci(step),
|
||||
`data-type` = dataType,
|
||||
`time-format` = timeFormat,
|
||||
timezone = timezone
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -576,7 +576,7 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' @template update-input
|
||||
#' @inheritParams selectInput
|
||||
#'
|
||||
#' @seealso \code{\link{selectInput}}
|
||||
#' @seealso \code{\link{selectInput}} \code{\link{varSelectInput}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
@@ -642,8 +642,86 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
if (!server) {
|
||||
return(updateSelectInput(session, inputId, label, choices, selected))
|
||||
}
|
||||
|
||||
noOptGroup <- TRUE
|
||||
if (is.list(choices)) {
|
||||
# check if list is nested
|
||||
for (i in seq_along(choices)) {
|
||||
if (is.list(choices[[i]]) || length(choices[[i]]) > 1) {
|
||||
noOptGroup <- FALSE
|
||||
break()
|
||||
}
|
||||
}
|
||||
}
|
||||
# convert choices to a data frame so it returns [{label: , value: , group: },...]
|
||||
choices <- if (is.atomic(choices) || noOptGroup) {
|
||||
# fast path for vectors and flat lists
|
||||
if (is.list(choices)) {
|
||||
choices <- unlist(choices)
|
||||
}
|
||||
if (is.null(names(choices))) {
|
||||
lab <- as.character(choices)
|
||||
} else {
|
||||
lab <- names(choices)
|
||||
# replace empty names like: choices = c(a = 1, 2)
|
||||
# in this case: names(choices) = c("a", "")
|
||||
# with replacement below choices will be: lab = c("a", "2")
|
||||
empty_names_indices <- lab == ""
|
||||
lab[empty_names_indices] <- as.character(choices[empty_names_indices])
|
||||
}
|
||||
data.frame(label = lab, value = choices, stringsAsFactors = FALSE)
|
||||
} else {
|
||||
# slow path for nested lists/optgroups
|
||||
list_names <- names(choices)
|
||||
if (is.null(list_names)) {
|
||||
list_names <- rep("", length(choices))
|
||||
}
|
||||
|
||||
choice_list <- mapply(choices, list_names, FUN = function (choice, name) {
|
||||
group <- ""
|
||||
lab <- name
|
||||
if (lab == "") lab <- as.character(choice)
|
||||
|
||||
if (is.list(choice) || length(choice) > 1) {
|
||||
group <- rep(name, length(choice))
|
||||
choice <- unlist(choice)
|
||||
|
||||
if (is.null(names(choice))) {
|
||||
lab <- as.character(choice)
|
||||
} else {
|
||||
lab <- names(choice)
|
||||
# replace empty names like: choices = c(a = 1, 2)
|
||||
# in this case: names(choices) = c("a", "")
|
||||
# with replacement below choices will be: lab = c("a", "2")
|
||||
empty_names_indices <- lab == ""
|
||||
lab[empty_names_indices] <- as.character(choice[empty_names_indices])
|
||||
}
|
||||
}
|
||||
|
||||
list(
|
||||
label = lab,
|
||||
value = as.character(choice),
|
||||
group = group
|
||||
)
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
|
||||
extract_vector <- function(x, name) {
|
||||
vecs <- lapply(x, `[[`, name)
|
||||
do.call(c, vecs)
|
||||
}
|
||||
|
||||
data.frame(
|
||||
label = extract_vector(choice_list, "label"),
|
||||
value = extract_vector(choice_list, "value"),
|
||||
group = extract_vector(choice_list, "group"),
|
||||
stringsAsFactors = FALSE, row.names = NULL
|
||||
)
|
||||
}
|
||||
|
||||
value <- unname(selected)
|
||||
attr(choices, 'selected_value') <- value
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label,
|
||||
value = value,
|
||||
@@ -651,38 +729,76 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
#' @rdname updateSelectInput
|
||||
#' @inheritParams varSelectInput
|
||||
#' @export
|
||||
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
choices <- colnames(data)
|
||||
}
|
||||
updateSelectInput(
|
||||
session = session,
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected
|
||||
)
|
||||
}
|
||||
#' @rdname updateSelectInput
|
||||
#' @export
|
||||
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
choices <- colnames(data)
|
||||
}
|
||||
updateSelectizeInput(
|
||||
session = session,
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected,
|
||||
options = options,
|
||||
server = server
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
selectizeJSON <- function(data, req) {
|
||||
query <- parseQueryString(req$QUERY_STRING)
|
||||
|
||||
# extract the query variables, conjunction (and/or), search string, maximum options
|
||||
var <- c(safeFromJSON(query$field))
|
||||
cjn <- if (query$conju == 'and') all else any
|
||||
|
||||
# all keywords in lower-case, for case-insensitive matching
|
||||
key <- unique(strsplit(tolower(query$query), '\\s+')[[1]])
|
||||
|
||||
if (identical(key, '')) key <- character(0)
|
||||
mop <- as.numeric(query$maxop)
|
||||
vfd <- query$value # the value field name
|
||||
sel <- attr(data, 'selected_value', exact = TRUE)
|
||||
|
||||
# convert a single vector to a data frame so it returns {label: , value: }
|
||||
# later in JSON; other objects return arbitrary JSON {x: , y: , foo: , ...}
|
||||
data <- if (is.atomic(data)) {
|
||||
data.frame(label = names(choicesWithNames(data)), value = data,
|
||||
stringsAsFactors = FALSE)
|
||||
} else as.data.frame(data, stringsAsFactors = FALSE)
|
||||
|
||||
# start searching for keywords in all specified columns
|
||||
idx <- logical(nrow(data))
|
||||
if (length(key)) for (v in var) {
|
||||
matches <- do.call(
|
||||
cbind,
|
||||
lapply(key, function(k) {
|
||||
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
|
||||
})
|
||||
)
|
||||
# merge column matches using OR, and match multiple keywords in one column
|
||||
# using the conjunction setting (AND or OR)
|
||||
idx <- idx | apply(matches, 1, cjn)
|
||||
if (length(key)) {
|
||||
for (v in var) {
|
||||
matches <- do.call(
|
||||
cbind,
|
||||
lapply(key, function(k) {
|
||||
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
|
||||
})
|
||||
)
|
||||
# merge column matches using OR, and match multiple keywords in one column
|
||||
# using the conjunction setting (AND or OR)
|
||||
matches <- rowSums(matches)
|
||||
if (query$conju == 'and')
|
||||
idx <- idx | (matches == length(key))
|
||||
else
|
||||
idx <- idx | matches
|
||||
}
|
||||
}
|
||||
# only return the first n rows (n = maximum options in configuration)
|
||||
idx <- utils::head(if (length(key)) which(idx) else seq_along(idx), mop)
|
||||
|
||||
164
R/utils.R
164
R/utils.R
@@ -269,6 +269,25 @@ dirExists <- function(paths) {
|
||||
file.exists(paths) & file.info(paths)$isdir
|
||||
}
|
||||
|
||||
# Removes empty directory (vectorized). This is needed because file.remove()
|
||||
# on Unix will remove empty directories, but on Windows, it will not. On
|
||||
# Windows, you would need to use unlink(recursive=TRUE), which is not very
|
||||
# safe. This function does it safely on Unix and Windows.
|
||||
dirRemove <- function(path) {
|
||||
for (p in path) {
|
||||
if (!dirExists(p)) {
|
||||
stop("Cannot remove non-existent directory ", p, ".")
|
||||
}
|
||||
if (length(dir(p, all.files = TRUE, no.. = TRUE)) != 0) {
|
||||
stop("Cannot remove non-empty directory ", p, ".")
|
||||
}
|
||||
result <- unlink(p, recursive = TRUE)
|
||||
if (result == 1) {
|
||||
stop("Error removing directory ", p, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Attempt to join a path and relative path, and turn the result into a
|
||||
# (normalized) absolute path. The result will only be returned if it is an
|
||||
# existing file/directory and is a descendant of dir.
|
||||
@@ -1532,7 +1551,10 @@ writeUTF8 <- function(text, ...) {
|
||||
writeLines(text, ..., useBytes = TRUE)
|
||||
}
|
||||
|
||||
URLdecode <- decodeURIComponent
|
||||
URLdecode <- function(value) {
|
||||
decodeURIComponent(value)
|
||||
}
|
||||
|
||||
URLencode <- function(value, reserved = FALSE) {
|
||||
value <- enc2utf8(value)
|
||||
if (reserved) encodeURIComponent(value) else encodeURI(value)
|
||||
@@ -1578,3 +1600,143 @@ Mutable <- R6Class("Mutable",
|
||||
get = function() { private$value }
|
||||
)
|
||||
)
|
||||
|
||||
# More convenient way of chaining together promises than then/catch/finally,
|
||||
# without the performance impact of %...>%.
|
||||
promise_chain <- function(promise, ..., catch = NULL, finally = NULL,
|
||||
domain = NULL, replace = FALSE) {
|
||||
|
||||
do <- function() {
|
||||
p <- Reduce(function(memo, func) {
|
||||
promises::then(memo, func)
|
||||
}, list(...), promise)
|
||||
|
||||
if (!is.null(catch)) {
|
||||
p <- promises::catch(p, catch)
|
||||
}
|
||||
|
||||
if (!is.null(finally)) {
|
||||
p <- promises::finally(p, finally)
|
||||
}
|
||||
|
||||
p
|
||||
}
|
||||
|
||||
if (!is.null(domain)) {
|
||||
promises::with_promise_domain(domain, do(), replace = replace)
|
||||
} else {
|
||||
do()
|
||||
}
|
||||
}
|
||||
|
||||
# Like promise_chain, but if `expr` returns a non-promise, then `...`, `catch`,
|
||||
# and `finally` are all executed synchronously
|
||||
hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
|
||||
domain = NULL, replace = FALSE) {
|
||||
|
||||
do <- function() {
|
||||
runFinally <- TRUE
|
||||
tryCatch(
|
||||
{
|
||||
captureStackTraces({
|
||||
result <- withVisible(force(expr))
|
||||
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(setVisible(result), ..., catch = catch, finally = finally)
|
||||
runFinally <- FALSE
|
||||
p
|
||||
} else {
|
||||
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)
|
||||
|
||||
setVisible(result)
|
||||
}
|
||||
})
|
||||
},
|
||||
error = function(e) {
|
||||
if (!is.null(catch))
|
||||
catch(e)
|
||||
else
|
||||
stop(e)
|
||||
},
|
||||
finally = if (runFinally && !is.null(finally)) finally()
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(domain)) {
|
||||
promises::with_promise_domain(domain, do(), replace = replace)
|
||||
} else {
|
||||
do()
|
||||
}
|
||||
}
|
||||
|
||||
# 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)
|
||||
force(name)
|
||||
force(value)
|
||||
|
||||
promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
function(...) {
|
||||
orig <- env[[name]]
|
||||
env[[name]] <- value
|
||||
on.exit(env[[name]] <- orig)
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
function(...) {
|
||||
orig <- env[[name]]
|
||||
env[[name]] <- value
|
||||
on.exit(env[[name]] <- orig)
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
orig <- env[[name]]
|
||||
env[[name]] <- value
|
||||
on.exit(env[[name]] <- orig)
|
||||
|
||||
force(expr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getSliderType <- function(min, max, value) {
|
||||
vals <- dropNulls(list(value, min, max))
|
||||
type <- unique(lapply(vals, function(x) {
|
||||
if (inherits(x, "Date")) "date"
|
||||
else if (inherits(x, "POSIXt")) "datetime"
|
||||
else "number"
|
||||
}))
|
||||
if (length(type) > 1) {
|
||||
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
|
||||
}
|
||||
type[[1]]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ Shiny is a new package from RStudio that makes it incredibly easy to build inter
|
||||
|
||||
For an introduction and examples, visit the [Shiny Dev Center](http://shiny.rstudio.com/).
|
||||
|
||||
If you have general questions about using Shiny, please use the [RStudio Community website](https://community.rstudio.com). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
|
||||
## Features
|
||||
|
||||
* Build useful web applications with only a few lines of code—no JavaScript required.
|
||||
@@ -41,8 +43,6 @@ devtools::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.
|
||||
|
||||
We hope you enjoy using Shiny. If you have general questions about using Shiny, please use the Shiny [mailing list](https://groups.google.com/forum/#!forum/shiny-discuss). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
|
||||
## 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.
|
||||
|
||||
54
TODO-promises.md
Normal file
54
TODO-promises.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Promises TODO
|
||||
|
||||
## Documentation
|
||||
|
||||
- [x] Motivation -- why should I care about async? Why shouldn't I (what are the limitations)?
|
||||
- [x] High level technical overview
|
||||
- [ ] Cookbook-style examples
|
||||
- [ ] Top-down porting of a sync app to async
|
||||
|
||||
## Core API
|
||||
- [x] Should as.promise() convert regular values to promises? Or throw?
|
||||
- [x] If as.promise() doesn't convert regular values to promises, add promise_resolved(value) and promise_rejected(err) functions?
|
||||
|
||||
## later
|
||||
- [ ] Add support for multiple event loops
|
||||
- [x] Add timeout to run_now
|
||||
|
||||
## Error handling/debugging
|
||||
- [ ] ..stacktraceon../..stacktraceoff.. and stack traces in general
|
||||
- [x] long stack traces
|
||||
- [x] require opt-in
|
||||
- [ ] options(shiny.error) should work in promise handlers
|
||||
- [x] Detect when reactives are used across process boundaries, and error
|
||||
|
||||
## Render functions
|
||||
- [x] Non-async render functions should have their code all execute on the current tick. Otherwise order of execution will be surprising if they have side effects and explicit priorities.
|
||||
- [x] Promise domains should maybe have an onExecute, for the "sync" part that kicks off async operations to also have wrapping behavior (like capturing output). Right now, I have to start off renderPrint with promise(~resolve(TRUE)) and then execute the user code in a then(), just to get the promise behavior. Same will be true when we tackle error handling (stack trace capture).
|
||||
- [x] invisible() doesn't seem to be working correctly with renderPrint. .visible doesn't survive promise chaining, e.g. promise(~resolve(promise(~resolve(invisible("Hi"))))) %>% then(function(x, .visible) { cat(.visible) }) will print TRUE, not FALSE.
|
||||
- [x] renderDataTable should support async
|
||||
- [x] Support downloadHandler
|
||||
- [ ] Support async filename?
|
||||
- [x] Should prevent session from continuing until download completes (ref count)
|
||||
|
||||
## Flush lifecycle
|
||||
- [x] While async operations are running in a session, hold off on any further processing of inputs and scheduled task items until all operations are complete.
|
||||
- [x] Hold all outputs/errors until async operations are complete.
|
||||
- [ ] Allow both sync and async outputs to be displayed before all outputs are done. (opt-in)
|
||||
|
||||
## Testing
|
||||
- [x] App that tests that all built-in render functions support async
|
||||
- [x] Apps that test flush lifecycle, including onFlushed(once = FALSE)
|
||||
- [x] Apps that test invisible() behavior for renderPrint, both sync and async
|
||||
- [x] Apps that ensure all render functions execute synchronous code before tick is over
|
||||
- [x] App that tests async downloadHandler
|
||||
- [x] App that verifies inputs/timers don't fire for a session while it has async operations pending
|
||||
- [x] App that verifies req(FALSE), req(FALSE, cancelOutput = TRUE), validate/need, etc. all work in async
|
||||
|
||||
## External packages
|
||||
- [x] DT
|
||||
- [x] htmlwidgets: Don't require async-aware version of Shiny if not using async
|
||||
- [x] Plotly
|
||||
|
||||
## Bugs
|
||||
- [x] req(FALSE, cancelOutput = TRUE) shows grey (even without async)
|
||||
@@ -43,3 +43,7 @@ artifacts:
|
||||
|
||||
- path: '\*_*.zip'
|
||||
name: Bits
|
||||
|
||||
environment:
|
||||
global:
|
||||
USE_RTOOLS: true
|
||||
|
||||
@@ -41,6 +41,7 @@ sd_section("UI Inputs",
|
||||
"numericInput",
|
||||
"radioButtons",
|
||||
"selectInput",
|
||||
"varSelectInput",
|
||||
"sliderInput",
|
||||
"submitButton",
|
||||
"textInput",
|
||||
@@ -104,6 +105,7 @@ sd_section("Rendering functions",
|
||||
"Functions that you use in your application's server side code, assigning them to outputs that appear in your user interface.",
|
||||
c(
|
||||
"renderPlot",
|
||||
"renderCachedPlot",
|
||||
"renderText",
|
||||
"renderPrint",
|
||||
"renderDataTable",
|
||||
@@ -115,7 +117,8 @@ sd_section("Rendering functions",
|
||||
"reactivePrint",
|
||||
"reactiveTable",
|
||||
"reactiveText",
|
||||
"reactiveUI"
|
||||
"reactiveUI",
|
||||
"createRenderFunction"
|
||||
)
|
||||
)
|
||||
sd_section("Reactive programming",
|
||||
@@ -194,7 +197,9 @@ sd_section("Utility functions",
|
||||
"exprToFunction",
|
||||
"installExprFunction",
|
||||
"parseQueryString",
|
||||
"getCurrentOutputInfo",
|
||||
"plotPNG",
|
||||
"sizeGrowthRatio",
|
||||
"exportTestValues",
|
||||
"setSerializer",
|
||||
"snapshotExclude",
|
||||
@@ -205,7 +210,10 @@ sd_section("Utility functions",
|
||||
"shinyDeprecated",
|
||||
"serverInfo",
|
||||
"shiny-options",
|
||||
"onStop"
|
||||
"onStop",
|
||||
"diskCache",
|
||||
"memoryCache",
|
||||
"key_missing"
|
||||
)
|
||||
)
|
||||
sd_section("Plot interaction",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
|
||||
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
|
||||
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400,600' rel='stylesheet' type='text/css'>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
|
||||
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
|
||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400,600' rel='stylesheet' type='text/css'>
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
|
||||
108
inst/www/shared/ionrangeslider/css/ion.rangeSlider.skinRound.css
Normal file
108
inst/www/shared/ionrangeslider/css/ion.rangeSlider.skinRound.css
Normal file
@@ -0,0 +1,108 @@
|
||||
/* Ion.RangeSlider, Round Skin
|
||||
// css version 2.2.0
|
||||
// © Denis Ineshin, 2014 https://github.com/IonDen
|
||||
// © Veaceslav Grimalschi, 2018 https://github.com/grimalschi
|
||||
// ===================================================================================================================*/
|
||||
|
||||
/* =====================================================================================================================
|
||||
// Skin details */
|
||||
|
||||
.irs {
|
||||
height: 50px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.irs-with-grid {
|
||||
height: 67px;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
top: 36px;
|
||||
height: 4px;
|
||||
background: #DEE4EC;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.irs-bar, .irs-bar-edge {
|
||||
top: 36px;
|
||||
height: 4px;
|
||||
background: #006CFA;
|
||||
}
|
||||
.irs-bar-edge {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
height: 4px;
|
||||
top: 40px;
|
||||
background: #DEE4EC;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=25);
|
||||
}
|
||||
|
||||
.irs-slider {
|
||||
top: 35px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -10px;
|
||||
border: 4px solid #006CFA;
|
||||
background: white;
|
||||
border-radius: 27px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,255,0.3);
|
||||
cursor: pointer;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.irs-slider.state_hover, .irs-slider:hover {
|
||||
background: #f0f6ff;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
top: 0;
|
||||
padding: 3px 5px;
|
||||
background: rgba(0,0,0,0.1);
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
text-shadow: none;
|
||||
padding: 3px 5px;
|
||||
background: #006CFA;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
.irs-from:after, .irs-to:after, .irs-single:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: -3px;
|
||||
overflow: hidden;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #006CFA;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
.irs-grid-pol {
|
||||
background: #DEE4EC;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
bottom: 4px;
|
||||
color: silver;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Ion.RangeSlider, Square Skin
|
||||
// css version 2.2.0
|
||||
// © Denis Ineshin, 2014 https://github.com/IonDen
|
||||
// © Veaceslav Grimalschi, 2018 https://github.com/grimalschi
|
||||
// ===================================================================================================================*/
|
||||
|
||||
/* =====================================================================================================================
|
||||
// Skin details */
|
||||
|
||||
.irs {
|
||||
height: 45px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.irs-with-grid {
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
top: 31px;
|
||||
height: 4px;
|
||||
background: #DEDEDE;
|
||||
}
|
||||
|
||||
.irs-bar, .irs-bar-edge {
|
||||
top: 31px;
|
||||
height: 4px;
|
||||
background: black;
|
||||
}
|
||||
.irs-bar-edge {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
height: 2px;
|
||||
top: 37px;
|
||||
background: #DEDEDE;
|
||||
}
|
||||
|
||||
.irs-slider {
|
||||
top: 30px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: -5px;
|
||||
border: 3px solid black;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
box-sizing: content-box;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.irs-slider.state_hover, .irs-slider:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
top: 0;
|
||||
padding: 3px 4px;
|
||||
background: rgba(0,0,0,0.1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
text-shadow: none;
|
||||
padding: 3px 4px;
|
||||
background: black;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
.irs-grid-pol {
|
||||
background: #DEDEDE;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
bottom: 4px;
|
||||
color: silver;
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Ion.RangeSlider
|
||||
// version 2.1.6 Build: 369
|
||||
// © Denis Ineshin, 2016
|
||||
// version 2.2.0 Build: 380
|
||||
// © Denis Ineshin, 2017
|
||||
// https://github.com/IonDen
|
||||
//
|
||||
// Project page: http://ionden.com/a/plugins/ion.rangeSlider/en.html
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
var base_html =
|
||||
'<span class="irs">' +
|
||||
'<span class="irs-line" tabindex="-1"><span class="irs-line-left"></span><span class="irs-line-mid"></span><span class="irs-line-right"></span></span>' +
|
||||
'<span class="irs-line" tabindex="0"><span class="irs-line-left"></span><span class="irs-line-mid"></span><span class="irs-line-right"></span></span>' +
|
||||
'<span class="irs-min">0</span><span class="irs-max">1</span>' +
|
||||
'<span class="irs-from">0</span><span class="irs-to">0</span><span class="irs-single">0</span>' +
|
||||
'</span>' +
|
||||
@@ -156,7 +156,7 @@
|
||||
* @constructor
|
||||
*/
|
||||
var IonRangeSlider = function (input, options, plugin_count) {
|
||||
this.VERSION = "2.1.6";
|
||||
this.VERSION = "2.2.0";
|
||||
this.input = input;
|
||||
this.plugin_count = plugin_count;
|
||||
this.current_plugin = 0;
|
||||
@@ -169,9 +169,9 @@
|
||||
this.dragging = false;
|
||||
this.force_redraw = false;
|
||||
this.no_diapason = false;
|
||||
this.has_tab_index = true;
|
||||
this.is_key = false;
|
||||
this.is_update = false;
|
||||
this.is_first_update = true;
|
||||
this.is_start = true;
|
||||
this.is_finish = false;
|
||||
this.is_active = false;
|
||||
@@ -303,8 +303,7 @@
|
||||
|
||||
force_edges: false,
|
||||
|
||||
keyboard: false,
|
||||
keyboard_step: 5,
|
||||
keyboard: true,
|
||||
|
||||
grid: false,
|
||||
grid_margin: true,
|
||||
@@ -323,7 +322,11 @@
|
||||
input_values_separator: ";",
|
||||
|
||||
disable: false,
|
||||
block: false,
|
||||
|
||||
extra_classes: "",
|
||||
|
||||
scope: null,
|
||||
onStart: null,
|
||||
onChange: null,
|
||||
onFinish: null,
|
||||
@@ -369,7 +372,6 @@
|
||||
force_edges: $inp.data("forceEdges"),
|
||||
|
||||
keyboard: $inp.data("keyboard"),
|
||||
keyboard_step: $inp.data("keyboardStep"),
|
||||
|
||||
grid: $inp.data("grid"),
|
||||
grid_margin: $inp.data("gridMargin"),
|
||||
@@ -387,7 +389,10 @@
|
||||
|
||||
input_values_separator: $inp.data("inputValuesSeparator"),
|
||||
|
||||
disable: $inp.data("disable")
|
||||
disable: $inp.data("disable"),
|
||||
block: $inp.data("block"),
|
||||
|
||||
extra_classes: $inp.data("extraClasses"),
|
||||
};
|
||||
config_from_data.values = config_from_data.values && config_from_data.values.split(",");
|
||||
|
||||
@@ -498,7 +503,7 @@
|
||||
* Appends slider template to a DOM
|
||||
*/
|
||||
append: function () {
|
||||
var container_html = '<span class="irs js-irs-' + this.plugin_count + '"></span>';
|
||||
var container_html = '<span class="irs js-irs-' + this.plugin_count + ' ' + this.options.extra_classes + '"></span>';
|
||||
this.$cache.input.before(container_html);
|
||||
this.$cache.input.prop("readonly", true);
|
||||
this.$cache.cont = this.$cache.input.prev();
|
||||
@@ -544,11 +549,20 @@
|
||||
this.appendDisableMask();
|
||||
this.$cache.input[0].disabled = true;
|
||||
} else {
|
||||
this.$cache.cont.removeClass("irs-disabled");
|
||||
this.$cache.input[0].disabled = false;
|
||||
this.removeDisableMask();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
// block only if not disabled
|
||||
if (!this.options.disable) {
|
||||
if (this.options.block) {
|
||||
this.appendDisableMask();
|
||||
} else {
|
||||
this.removeDisableMask();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.drag_interval) {
|
||||
this.$cache.bar[0].style.cursor = "ew-resize";
|
||||
}
|
||||
@@ -581,6 +595,7 @@
|
||||
switch (target) {
|
||||
case "single":
|
||||
this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_single_fake);
|
||||
this.$cache.s_single.addClass("state_hover");
|
||||
break;
|
||||
case "from":
|
||||
this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake);
|
||||
@@ -612,9 +627,18 @@
|
||||
this.$cache.cont.addClass("irs-disabled");
|
||||
},
|
||||
|
||||
/**
|
||||
* Then slider is not disabled
|
||||
* remove disable mask
|
||||
*/
|
||||
removeDisableMask: function () {
|
||||
this.$cache.cont.remove(".irs-disable-mask");
|
||||
this.$cache.cont.removeClass("irs-disabled");
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove slider instance
|
||||
* and ubind all events
|
||||
* and unbind all events
|
||||
*/
|
||||
remove: function () {
|
||||
this.$cache.cont.remove();
|
||||
@@ -659,6 +683,8 @@
|
||||
this.$cache.line.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
|
||||
this.$cache.line.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
|
||||
|
||||
this.$cache.line.on("focus.irs_" + this.plugin_count, this.pointerFocus.bind(this));
|
||||
|
||||
if (this.options.drag_interval && this.options.type === "double") {
|
||||
this.$cache.bar.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "both"));
|
||||
this.$cache.bar.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "both"));
|
||||
@@ -705,6 +731,29 @@
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus with tabIndex
|
||||
*
|
||||
* @param e {Object} event object
|
||||
*/
|
||||
pointerFocus: function (e) {
|
||||
if (!this.target) {
|
||||
var x;
|
||||
var $handle;
|
||||
|
||||
if (this.options.type === "single") {
|
||||
$handle = this.$cache.single;
|
||||
} else {
|
||||
$handle = this.$cache.from;
|
||||
}
|
||||
|
||||
x = $handle.offset().left;
|
||||
x += ($handle.width() / 2) - 1;
|
||||
|
||||
this.pointerClick("single", {preventDefault: function () {}, pageX: x});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mousemove or touchmove
|
||||
* only for handlers
|
||||
@@ -864,18 +913,19 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Move by key. Beta
|
||||
* @todo refactor than have plenty of time
|
||||
* Move by key
|
||||
*
|
||||
* @param right {boolean} direction to move
|
||||
*/
|
||||
moveByKey: function (right) {
|
||||
var p = this.coords.p_pointer;
|
||||
var p_step = (this.options.max - this.options.min) / 100;
|
||||
p_step = this.options.step / p_step;
|
||||
|
||||
if (right) {
|
||||
p += this.options.keyboard_step;
|
||||
p += p_step;
|
||||
} else {
|
||||
p -= this.options.keyboard_step;
|
||||
p -= p_step;
|
||||
}
|
||||
|
||||
this.coords.x_pointer = this.toFixed(this.coords.w_rs / 100 * p);
|
||||
@@ -902,8 +952,14 @@
|
||||
this.$cache.min.html(this.decorate(this.options.p_values[this.options.min]));
|
||||
this.$cache.max.html(this.decorate(this.options.p_values[this.options.max]));
|
||||
} else {
|
||||
this.$cache.min.html(this.decorate(this._prettify(this.options.min), this.options.min));
|
||||
this.$cache.max.html(this.decorate(this._prettify(this.options.max), this.options.max));
|
||||
var min_pretty = this._prettify(this.options.min);
|
||||
var max_pretty = this._prettify(this.options.max);
|
||||
|
||||
this.result.min_pretty = min_pretty;
|
||||
this.result.max_pretty = max_pretty;
|
||||
|
||||
this.$cache.min.html(this.decorate(min_pretty, this.options.min));
|
||||
this.$cache.max.html(this.decorate(max_pretty, this.options.max));
|
||||
}
|
||||
|
||||
this.labels.w_min = this.$cache.min.outerWidth(false);
|
||||
@@ -1114,6 +1170,7 @@
|
||||
|
||||
this.result.from_percent = this.coords.p_single_real;
|
||||
this.result.from = this.convertToValue(this.coords.p_single_real);
|
||||
this.result.from_pretty = this._prettify(this.result.from);
|
||||
|
||||
if (this.options.values.length) {
|
||||
this.result.from_value = this.options.values[this.result.from];
|
||||
@@ -1124,8 +1181,10 @@
|
||||
|
||||
this.result.from_percent = this.coords.p_from_real;
|
||||
this.result.from = this.convertToValue(this.coords.p_from_real);
|
||||
this.result.from_pretty = this._prettify(this.result.from);
|
||||
this.result.to_percent = this.coords.p_to_real;
|
||||
this.result.to = this.convertToValue(this.coords.p_to_real);
|
||||
this.result.to_pretty = this._prettify(this.result.to);
|
||||
|
||||
if (this.options.values.length) {
|
||||
this.result.from_value = this.options.values[this.result.from];
|
||||
@@ -1364,10 +1423,9 @@
|
||||
if (!this.is_resize && !this.is_update && !this.is_start && !this.is_finish) {
|
||||
this.callOnChange();
|
||||
}
|
||||
if (this.is_key || this.is_click || this.is_first_update) {
|
||||
if (this.is_key || this.is_click) {
|
||||
this.is_key = false;
|
||||
this.is_click = false;
|
||||
this.is_first_update = false;
|
||||
this.callOnFinish();
|
||||
}
|
||||
|
||||
@@ -1392,11 +1450,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var values_num = this.options.values.length,
|
||||
p_values = this.options.p_values,
|
||||
text_single,
|
||||
text_from,
|
||||
text_to;
|
||||
var values_num = this.options.values.length;
|
||||
var p_values = this.options.p_values;
|
||||
var text_single;
|
||||
var text_from;
|
||||
var text_to;
|
||||
var from_pretty;
|
||||
var to_pretty;
|
||||
|
||||
if (this.options.hide_from_to) {
|
||||
return;
|
||||
@@ -1408,7 +1468,9 @@
|
||||
text_single = this.decorate(p_values[this.result.from]);
|
||||
this.$cache.single.html(text_single);
|
||||
} else {
|
||||
text_single = this.decorate(this._prettify(this.result.from), this.result.from);
|
||||
from_pretty = this._prettify(this.result.from);
|
||||
|
||||
text_single = this.decorate(from_pretty, this.result.from);
|
||||
this.$cache.single.html(text_single);
|
||||
}
|
||||
|
||||
@@ -1445,16 +1507,18 @@
|
||||
this.$cache.to.html(text_to);
|
||||
|
||||
} else {
|
||||
from_pretty = this._prettify(this.result.from);
|
||||
to_pretty = this._prettify(this.result.to);
|
||||
|
||||
if (this.options.decorate_both) {
|
||||
text_single = this.decorate(this._prettify(this.result.from), this.result.from);
|
||||
text_single = this.decorate(from_pretty, this.result.from);
|
||||
text_single += this.options.values_separator;
|
||||
text_single += this.decorate(this._prettify(this.result.to), this.result.to);
|
||||
text_single += this.decorate(to_pretty, this.result.to);
|
||||
} else {
|
||||
text_single = this.decorate(this._prettify(this.result.from) + this.options.values_separator + this._prettify(this.result.to), this.result.to);
|
||||
text_single = this.decorate(from_pretty + this.options.values_separator + to_pretty, this.result.to);
|
||||
}
|
||||
text_from = this.decorate(this._prettify(this.result.from), this.result.from);
|
||||
text_to = this.decorate(this._prettify(this.result.to), this.result.to);
|
||||
text_from = this.decorate(from_pretty, this.result.from);
|
||||
text_to = this.decorate(to_pretty, this.result.to);
|
||||
|
||||
this.$cache.single.html(text_single);
|
||||
this.$cache.from.html(text_from);
|
||||
@@ -1606,28 +1670,44 @@
|
||||
this.writeToInput();
|
||||
|
||||
if (this.options.onStart && typeof this.options.onStart === "function") {
|
||||
this.options.onStart(this.result);
|
||||
if (this.options.scope) {
|
||||
this.options.onStart.call(this.options.scope, this.result);
|
||||
} else {
|
||||
this.options.onStart(this.result);
|
||||
}
|
||||
}
|
||||
},
|
||||
callOnChange: function () {
|
||||
this.writeToInput();
|
||||
|
||||
if (this.options.onChange && typeof this.options.onChange === "function") {
|
||||
this.options.onChange(this.result);
|
||||
if (this.options.scope) {
|
||||
this.options.onChange.call(this.options.scope, this.result);
|
||||
} else {
|
||||
this.options.onChange(this.result);
|
||||
}
|
||||
}
|
||||
},
|
||||
callOnFinish: function () {
|
||||
this.writeToInput();
|
||||
|
||||
if (this.options.onFinish && typeof this.options.onFinish === "function") {
|
||||
this.options.onFinish(this.result);
|
||||
if (this.options.scope) {
|
||||
this.options.onFinish.call(this.options.scope, this.result);
|
||||
} else {
|
||||
this.options.onFinish(this.result);
|
||||
}
|
||||
}
|
||||
},
|
||||
callOnUpdate: function () {
|
||||
this.writeToInput();
|
||||
|
||||
if (this.options.onUpdate && typeof this.options.onUpdate === "function") {
|
||||
this.options.onUpdate(this.result);
|
||||
if (this.options.scope) {
|
||||
this.options.onUpdate.call(this.options.scope, this.result);
|
||||
} else {
|
||||
this.options.onUpdate(this.result);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1639,6 +1719,14 @@
|
||||
|
||||
toggleInput: function () {
|
||||
this.$cache.input.toggleClass("irs-hidden-input");
|
||||
|
||||
if (this.has_tab_index) {
|
||||
this.$cache.input.prop("tabindex", -1);
|
||||
} else {
|
||||
this.$cache.input.removeProp("tabindex");
|
||||
}
|
||||
|
||||
this.has_tab_index = !this.has_tab_index;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1897,7 +1985,6 @@
|
||||
if (typeof o.to_min === "string") o.to_min = +o.to_min;
|
||||
if (typeof o.to_max === "string") o.to_max = +o.to_max;
|
||||
|
||||
if (typeof o.keyboard_step === "string") o.keyboard_step = +o.keyboard_step;
|
||||
if (typeof o.grid_num === "string") o.grid_num = +o.grid_num;
|
||||
|
||||
if (o.max < o.min) {
|
||||
@@ -1912,7 +1999,6 @@
|
||||
o.grid_num = o.max;
|
||||
o.grid_snap = true;
|
||||
|
||||
|
||||
for (i = 0; i < vl; i++) {
|
||||
value = +v[i];
|
||||
|
||||
@@ -1968,10 +2054,6 @@
|
||||
o.step = 1;
|
||||
}
|
||||
|
||||
if (typeof o.keyboard_step !== "number" || isNaN(o.keyboard_step) || !o.keyboard_step || o.keyboard_step < 0) {
|
||||
o.keyboard_step = 5;
|
||||
}
|
||||
|
||||
if (typeof o.from_min === "number" && o.from < o.from_min) {
|
||||
o.from = o.from_min;
|
||||
}
|
||||
@@ -2057,6 +2139,7 @@
|
||||
updateFrom: function () {
|
||||
this.result.from = this.options.from;
|
||||
this.result.from_percent = this.convertToPercent(this.result.from);
|
||||
this.result.from_pretty = this._prettify(this.result.from);
|
||||
if (this.options.values) {
|
||||
this.result.from_value = this.options.values[this.result.from];
|
||||
}
|
||||
@@ -2065,6 +2148,7 @@
|
||||
updateTo: function () {
|
||||
this.result.to = this.options.to;
|
||||
this.result.to_percent = this.convertToPercent(this.result.to);
|
||||
this.result.to_pretty = this._prettify(this.result.to);
|
||||
if (this.options.values) {
|
||||
this.result.to_value = this.options.values[this.result.to];
|
||||
}
|
||||
@@ -2107,8 +2191,15 @@
|
||||
this.calcGridMargin();
|
||||
|
||||
if (o.grid_snap) {
|
||||
big_num = total / o.step;
|
||||
big_p = this.toFixed(o.step / (total / 100));
|
||||
|
||||
if (total > 50) {
|
||||
big_num = 50 / o.step;
|
||||
big_p = this.toFixed(o.step / 0.5);
|
||||
} else {
|
||||
big_num = total / o.step;
|
||||
big_p = this.toFixed(o.step / (total / 100));
|
||||
}
|
||||
|
||||
} else {
|
||||
big_p = this.toFixed(100 / big_num);
|
||||
}
|
||||
@@ -2133,11 +2224,6 @@
|
||||
|
||||
if (big_w > 100) {
|
||||
big_w = 100;
|
||||
|
||||
local_small_max -= 2;
|
||||
if (local_small_max < 0) {
|
||||
local_small_max = 0;
|
||||
}
|
||||
}
|
||||
this.coords.big[i] = big_w;
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,6 +12,11 @@ pre.shiny-text-output.noplaceholder:empty {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -381,3 +386,10 @@ pre.shiny-text-output.noplaceholder:empty {
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(76, 174, 76, .6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
||||
|
||||
@@ -14,6 +14,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
var exports = window.Shiny = window.Shiny || {};
|
||||
|
||||
exports.version = "1.1.0.9000"; // Version number inserted by Grunt
|
||||
|
||||
var origPushState = window.history.pushState;
|
||||
window.history.pushState = function () {
|
||||
var result = origPushState.apply(this, arguments);
|
||||
@@ -184,7 +186,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
.replace(/[\b]/g, '\\b');
|
||||
|
||||
try {
|
||||
var func = new Function('with (this) {\n try {\n return (' + expr + ');\n } catch (e) {\n console.error(\'Error evaluating expression: ' + expr_escaped + '\');\n throw e;\n }\n }');
|
||||
var func = new Function("with (this) {\n try {\n return (" + expr + ");\n } catch (e) {\n console.error('Error evaluating expression: " + expr_escaped + "');\n throw e;\n }\n }");
|
||||
} catch (e) {
|
||||
console.error("Error parsing expression: " + expr);
|
||||
throw e;
|
||||
@@ -288,6 +290,33 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
return true;
|
||||
};
|
||||
|
||||
// Compare version strings like "1.0.1", "1.4-2". `op` must be a string like
|
||||
// "==" or "<".
|
||||
exports.compareVersion = function (a, op, b) {
|
||||
function versionParts(ver) {
|
||||
return (ver + "").replace(/-/, ".").replace(/(\.0)+[^\.]*$/, "").split(".");
|
||||
}
|
||||
|
||||
function cmpVersion(a, b) {
|
||||
a = versionParts(a);
|
||||
b = versionParts(b);
|
||||
var len = Math.min(a.length, b.length);
|
||||
var cmp;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
cmp = parseInt(a[i], 10) - parseInt(b[i], 10);
|
||||
if (cmp !== 0) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
return a.length - b.length;
|
||||
}
|
||||
|
||||
var diff = cmpVersion(a, b);
|
||||
|
||||
if (op === "==") return diff === 0;else if (op === ">=") return diff >= 0;else if (op === ">") return diff > 0;else if (op === "<=") return diff <= 0;else if (op === "<") return diff < 0;else throw "Unknown operator: " + op;
|
||||
};
|
||||
|
||||
// multimethod: Creates functions — "multimethods" — that are polymorphic on one
|
||||
// or more of their arguments.
|
||||
//
|
||||
@@ -461,7 +490,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
if (defaultMethod) {
|
||||
return defaultMethod.apply(invoke, args);
|
||||
} else {
|
||||
throw new Error('No method for dispatch value ' + dispatchVal);
|
||||
throw new Error("No method for dispatch value " + dispatchVal);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -733,26 +762,34 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.lastChanceCallback = [];
|
||||
};
|
||||
(function () {
|
||||
this.setInput = function (name, value) {
|
||||
var self = this;
|
||||
|
||||
this.setInput = function (name, value, opts) {
|
||||
this.pendingData[name] = value;
|
||||
|
||||
if (!this.timerId && !this.reentrant) {
|
||||
this.timerId = setTimeout(function () {
|
||||
self.reentrant = true;
|
||||
try {
|
||||
$.each(self.lastChanceCallback, function (i, callback) {
|
||||
callback();
|
||||
});
|
||||
self.timerId = null;
|
||||
var currentData = self.pendingData;
|
||||
self.pendingData = {};
|
||||
self.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
self.reentrant = false;
|
||||
}
|
||||
}, 0);
|
||||
if (!this.reentrant) {
|
||||
if (opts.priority === "event") {
|
||||
this.$sendNow();
|
||||
} else if (!this.timerId) {
|
||||
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$sendNow = function () {
|
||||
if (this.reentrant) {
|
||||
console.trace("Unexpected reentrancy in InputBatchSender!");
|
||||
}
|
||||
|
||||
this.reentrant = true;
|
||||
try {
|
||||
this.timerId = null;
|
||||
$.each(this.lastChanceCallback, function (i, callback) {
|
||||
callback();
|
||||
});
|
||||
var currentData = this.pendingData;
|
||||
this.pendingData = {};
|
||||
this.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
this.reentrant = false;
|
||||
}
|
||||
};
|
||||
}).call(InputBatchSender.prototype);
|
||||
@@ -762,11 +799,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.lastSentValues = this.reset(initialValues);
|
||||
};
|
||||
(function () {
|
||||
this.setInput = function (name, value) {
|
||||
// Note that opts is not passed to setInput at this stage of the input
|
||||
// decorator stack. If in the future this setInput keeps track of opts, it
|
||||
// would be best not to store the `el`, because that could prevent it from
|
||||
// being GC'd.
|
||||
this.setInput = function (name, value, opts) {
|
||||
var _splitInputNameType = splitInputNameType(name);
|
||||
|
||||
var inputName = _splitInputNameType.name;
|
||||
@@ -774,11 +807,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
var jsonValue = JSON.stringify(value);
|
||||
|
||||
if (this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
|
||||
if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
|
||||
return;
|
||||
}
|
||||
this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType };
|
||||
this.target.setInput(name, value);
|
||||
this.target.setInput(name, value, opts);
|
||||
};
|
||||
this.reset = function () {
|
||||
var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||||
@@ -821,6 +854,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
evt.value = value;
|
||||
evt.binding = opts.binding;
|
||||
evt.el = opts.el;
|
||||
evt.priority = opts.priority;
|
||||
|
||||
$(document).trigger(evt);
|
||||
|
||||
@@ -828,9 +862,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
name = evt.name;
|
||||
if (evt.inputType !== '') name += ':' + evt.inputType;
|
||||
|
||||
// opts aren't passed along to lower levels in the input decorator
|
||||
// Most opts aren't passed along to lower levels in the input decorator
|
||||
// stack.
|
||||
this.target.setInput(name, evt.value);
|
||||
this.target.setInput(name, evt.value, { priority: opts.priority });
|
||||
}
|
||||
};
|
||||
}).call(InputEventDecorator.prototype);
|
||||
@@ -843,7 +877,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.setInput = function (name, value, opts) {
|
||||
this.$ensureInit(name);
|
||||
|
||||
if (opts.immediate) this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
|
||||
if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
|
||||
};
|
||||
this.setRatePolicy = function (name, mode, millis) {
|
||||
if (mode === 'direct') {
|
||||
@@ -895,11 +929,25 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
// Merge opts with defaults, and return a new object.
|
||||
function addDefaultInputOpts(opts) {
|
||||
return $.extend({
|
||||
immediate: false,
|
||||
|
||||
opts = $.extend({
|
||||
priority: "immediate",
|
||||
binding: null,
|
||||
el: null
|
||||
}, opts);
|
||||
|
||||
if (opts && typeof opts.priority !== "undefined") {
|
||||
switch (opts.priority) {
|
||||
case "deferred":
|
||||
case "immediate":
|
||||
case "event":
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function splitInputNameType(name) {
|
||||
@@ -1236,17 +1284,22 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
};
|
||||
|
||||
this.receiveOutput = function (name, value) {
|
||||
if (this.$values[name] === value) return undefined;
|
||||
|
||||
this.$values[name] = value;
|
||||
delete this.$errors[name];
|
||||
|
||||
var binding = this.$bindings[name];
|
||||
var evt = jQuery.Event('shiny:value');
|
||||
evt.name = name;
|
||||
evt.value = value;
|
||||
evt.binding = binding;
|
||||
|
||||
if (this.$values[name] === value) {
|
||||
$(binding ? binding.el : document).trigger(evt);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.$values[name] = value;
|
||||
delete this.$errors[name];
|
||||
|
||||
$(binding ? binding.el : document).trigger(evt);
|
||||
|
||||
if (!evt.isDefaultPrevented() && binding) {
|
||||
binding.onValueChange(evt.value);
|
||||
}
|
||||
@@ -1942,7 +1995,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
// Progress bar starts hidden; will be made visible if a value is provided
|
||||
// during updates.
|
||||
exports.notifications.show({
|
||||
html: '<div id="shiny-progress-' + message.id + '" class="shiny-progress-notification">' + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
|
||||
html: "<div id=\"shiny-progress-" + message.id + "\" class=\"shiny-progress-notification\">" + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
|
||||
id: message.id,
|
||||
duration: null
|
||||
});
|
||||
@@ -2138,7 +2191,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
if ($notification.length === 0) $notification = _create(id);
|
||||
|
||||
// Render html and dependencies
|
||||
var newHtml = '<div class="shiny-notification-content-text">' + html + '</div>' + ('<div class="shiny-notification-content-action">' + action + '</div>');
|
||||
var newHtml = "<div class=\"shiny-notification-content-text\">" + html + "</div>" + ("<div class=\"shiny-notification-content-action\">" + action + "</div>");
|
||||
var $content = $notification.find('.shiny-notification-content');
|
||||
exports.renderContent($content, { html: newHtml, deps: deps });
|
||||
|
||||
@@ -2218,7 +2271,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
var $notification = _get(id);
|
||||
|
||||
if ($notification.length === 0) {
|
||||
$notification = $('<div id="shiny-notification-' + id + '" class="shiny-notification">' + '<div class="shiny-notification-close">×</div>' + '<div class="shiny-notification-content"></div>' + '</div>');
|
||||
$notification = $("<div id=\"shiny-notification-" + id + "\" class=\"shiny-notification\">" + '<div class="shiny-notification-close">×</div>' + '<div class="shiny-notification-content"></div>' + '</div>');
|
||||
|
||||
$notification.find('.shiny-notification-close').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
@@ -2934,7 +2987,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
return function (e) {
|
||||
if (e === null) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2942,7 +2995,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
// If outside of plotting region
|
||||
if (!coordmap.isInPanel(offset)) {
|
||||
if (nullOutside) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
if (clip) return;
|
||||
@@ -2963,8 +3016,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
coords.range = panel.range;
|
||||
coords.log = panel.log;
|
||||
|
||||
coords[".nonce"] = Math.random();
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords, { priority: "event" });
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -3151,7 +3203,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
// We're in a new or reset state
|
||||
if (isNaN(coords.xmin)) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
// Must tell other brushes to clear.
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: inputId, outputId: null
|
||||
@@ -3178,7 +3230,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
coords.outputId = outputId;
|
||||
|
||||
// Send data to server
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords);
|
||||
|
||||
$el.data("mostRecentBrush", true);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
|
||||
@@ -3812,7 +3864,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
};
|
||||
|
||||
exports.resetBrush = function (brushId) {
|
||||
exports.onInputChange(brushId, null);
|
||||
exports.setInputValue(brushId, null);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: brushId, outputId: null
|
||||
});
|
||||
@@ -3860,7 +3912,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
html = '';
|
||||
} else if (typeof content === 'string') {
|
||||
html = content;
|
||||
} else if ((typeof content === 'undefined' ? 'undefined' : _typeof(content)) === 'object') {
|
||||
} else if ((typeof content === "undefined" ? "undefined" : _typeof(content)) === 'object') {
|
||||
html = content.html;
|
||||
dependencies = content.deps || [];
|
||||
}
|
||||
@@ -4397,6 +4449,33 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
if (slider.$cache && slider.$cache.input) slider.$cache.input.trigger('change');else console.log("Couldn't force ion slider to update");
|
||||
}
|
||||
|
||||
function getTypePrettifyer(dataType, timeFormat, timezone) {
|
||||
var timeFormatter;
|
||||
var prettify;
|
||||
if (dataType === 'date') {
|
||||
timeFormatter = strftime.utc();
|
||||
prettify = function prettify(num) {
|
||||
return timeFormatter(timeFormat, new Date(num));
|
||||
};
|
||||
} else if (dataType === 'datetime') {
|
||||
if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
|
||||
|
||||
prettify = function prettify(num) {
|
||||
return timeFormatter(timeFormat, new Date(num));
|
||||
};
|
||||
} else {
|
||||
// The default prettify function for ion.rangeSlider adds thousands
|
||||
// separators after the decimal mark, so we have our own version here.
|
||||
// (#1958)
|
||||
prettify = function prettify(num) {
|
||||
// When executed, `this` will refer to the `IonRangeSlider.options`
|
||||
// object.
|
||||
return formatNumber(num, this.prettify_separator);
|
||||
};
|
||||
}
|
||||
return prettify;
|
||||
}
|
||||
|
||||
var sliderInputBinding = {};
|
||||
$.extend(sliderInputBinding, textInputBinding, {
|
||||
find: function find(scope) {
|
||||
@@ -4475,12 +4554,30 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
msg.from = data.value;
|
||||
}
|
||||
}
|
||||
if (data.hasOwnProperty('min')) msg.min = data.min;
|
||||
if (data.hasOwnProperty('max')) msg.max = data.max;
|
||||
if (data.hasOwnProperty('step')) msg.step = data.step;
|
||||
var sliderFeatures = ['min', 'max', 'step'];
|
||||
for (var i = 0; i < sliderFeatures.length; i++) {
|
||||
var feats = sliderFeatures[i];
|
||||
if (data.hasOwnProperty(feats)) {
|
||||
msg[feats] = data[feats];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('label')) $el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
|
||||
|
||||
var domElements = ['data-type', 'time-format', 'timezone'];
|
||||
for (var i = 0; i < domElements.length; i++) {
|
||||
var elem = domElements[i];
|
||||
if (data.hasOwnProperty(elem)) {
|
||||
$el.data(elem, data[elem]);
|
||||
}
|
||||
}
|
||||
|
||||
var dataType = $el.data('data-type');
|
||||
var timeFormat = $el.data('time-format');
|
||||
var timezone = $el.data('timezone');
|
||||
|
||||
msg.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
|
||||
|
||||
$el.data('immediate', true);
|
||||
try {
|
||||
slider.update(msg);
|
||||
@@ -4501,22 +4598,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
var $el = $(el);
|
||||
var dataType = $el.data('data-type');
|
||||
var timeFormat = $el.data('time-format');
|
||||
var timeFormatter;
|
||||
var timezone = $el.data('timezone');
|
||||
|
||||
// Set up formatting functions
|
||||
if (dataType === 'date') {
|
||||
timeFormatter = strftime.utc();
|
||||
opts.prettify = function (num) {
|
||||
return timeFormatter(timeFormat, new Date(num));
|
||||
};
|
||||
} else if (dataType === 'datetime') {
|
||||
var timezone = $el.data('timezone');
|
||||
if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
|
||||
|
||||
opts.prettify = function (num) {
|
||||
return timeFormatter(timeFormat, new Date(num));
|
||||
};
|
||||
}
|
||||
opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
|
||||
|
||||
$el.ionRangeSlider(opts);
|
||||
},
|
||||
@@ -4528,6 +4612,24 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
});
|
||||
inputBindings.register(sliderInputBinding, 'shiny.sliderInput');
|
||||
|
||||
// Format numbers for nicer output.
|
||||
// formatNumber(1234567.12345) === "1,234,567.12345"
|
||||
// formatNumber(1234567.12345, ".", ",") === "1.234.567,12345"
|
||||
// formatNumber(1000, " ") === "1 000"
|
||||
// formatNumber(20) === "20"
|
||||
// formatNumber(1.2345e24) === "1.2345e+24"
|
||||
function formatNumber(num) {
|
||||
var thousand_sep = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ",";
|
||||
var decimal_sep = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ".";
|
||||
|
||||
var parts = num.toString().split(".");
|
||||
|
||||
// Add separators to portion before decimal mark.
|
||||
parts[0] = parts[0].replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g, "$1" + thousand_sep);
|
||||
|
||||
if (parts.length === 1) return parts[0];else if (parts.length === 2) return parts[0] + decimal_sep + parts[1];else return "";
|
||||
};
|
||||
|
||||
$(document).on('click', '.slider-animate-button', function (evt) {
|
||||
evt.preventDefault();
|
||||
var self = $(this);
|
||||
@@ -4964,6 +5066,18 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
find: function find(scope) {
|
||||
return $(scope).find('select');
|
||||
},
|
||||
getType: function getType(el) {
|
||||
var $el = $(el);
|
||||
if (!$el.hasClass("symbol")) {
|
||||
// default character type
|
||||
return null;
|
||||
}
|
||||
if ($el.attr("multiple") === "multiple") {
|
||||
return 'shiny.symbolList';
|
||||
} else {
|
||||
return 'shiny.symbol';
|
||||
}
|
||||
},
|
||||
getId: function getId(el) {
|
||||
return InputBinding.prototype.getId.call(this, el) || el.name;
|
||||
},
|
||||
@@ -5015,8 +5129,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
if (data.hasOwnProperty('url')) {
|
||||
selectize = this._selectize(el);
|
||||
selectize.clearOptions();
|
||||
var thiz = this,
|
||||
loaded = false;
|
||||
var loaded = false;
|
||||
selectize.settings.load = function (query, callback) {
|
||||
var settings = selectize.settings;
|
||||
$.ajax({
|
||||
@@ -5033,8 +5146,19 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
callback();
|
||||
},
|
||||
success: function success(res) {
|
||||
// res = [{label: '1', value: '1', group: '1'}, ...]
|
||||
// success is called after options are added, but
|
||||
// groups need to be added manually below
|
||||
$.each(res, function (index, elem) {
|
||||
selectize.addOptionGroup(elem.group, { group: elem.group });
|
||||
});
|
||||
callback(res);
|
||||
if (!loaded && data.hasOwnProperty('value')) thiz.setValue(el, data.value);
|
||||
if (!loaded && data.hasOwnProperty('value')) {
|
||||
selectize.setValue(data.value);
|
||||
} else if (settings.maxItems === 1) {
|
||||
// first item selected by default only for single-select
|
||||
selectize.setValue(res[0].value);
|
||||
}
|
||||
loaded = true;
|
||||
}
|
||||
});
|
||||
@@ -5070,7 +5194,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
var options = $.extend({
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
searchField: ['label']
|
||||
searchField: ['label'],
|
||||
optgroupField: 'group',
|
||||
optgroupLabelField: 'group',
|
||||
optgroupValueField: 'group'
|
||||
}, JSON.parse(config.html()));
|
||||
// selectize created from selectInput()
|
||||
if (typeof config.data('nonempty') !== 'undefined') {
|
||||
@@ -5716,7 +5843,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
// Attach a dragenter handler to $el and all of its children. When the first
|
||||
// child is entered, trigger a draghoverstart event.
|
||||
$el.on("dragenter.dragHover", function (e) {
|
||||
if (collection.size() === 0) {
|
||||
if (collection.length === 0) {
|
||||
$el.trigger("draghoverstart" + ns, e.originalEvent);
|
||||
}
|
||||
// Every child that has fired dragenter is added to the collection.
|
||||
@@ -5731,7 +5858,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
collection = collection.not(e.originalEvent.target);
|
||||
// When the collection has no elements, all of the children have been
|
||||
// removed, and produce draghoverend event.
|
||||
if (collection.size() === 0) {
|
||||
if (collection.length === 0) {
|
||||
$el.trigger("draghoverend" + ns, e.originalEvent);
|
||||
}
|
||||
});
|
||||
@@ -6011,7 +6138,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
inputs = new InputValidateDecorator(inputs);
|
||||
|
||||
exports.onInputChange = function (name, value, opts) {
|
||||
exports.setInputValue = exports.onInputChange = function (name, value, opts) {
|
||||
opts = addDefaultInputOpts(opts);
|
||||
inputs.setInput(name, value, opts);
|
||||
};
|
||||
@@ -6025,7 +6152,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
var type = binding.getType(el);
|
||||
if (type) id = id + ":" + type;
|
||||
|
||||
var opts = { immediate: !allowDeferred, binding: binding, el: el };
|
||||
var opts = {
|
||||
priority: allowDeferred ? "deferred" : "immediate",
|
||||
binding: binding,
|
||||
el: el
|
||||
};
|
||||
inputs.setInput(id, value, opts);
|
||||
}
|
||||
}
|
||||
@@ -6188,7 +6319,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
// The server needs to know the size of each image and plot output element,
|
||||
// in case it is auto-sizing
|
||||
$('.shiny-image-output, .shiny-plot-output').each(function () {
|
||||
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
|
||||
var id = getIdFromEl(this);
|
||||
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
|
||||
initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
|
||||
@@ -6196,7 +6327,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
}
|
||||
});
|
||||
function doSendImageSize() {
|
||||
$('.shiny-image-output, .shiny-plot-output').each(function () {
|
||||
$('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
|
||||
var id = getIdFromEl(this);
|
||||
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
|
||||
inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);
|
||||
|
||||
File diff suppressed because one or more lines are too long
8
inst/www/shared/shiny.min.js
vendored
8
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,13 +5,13 @@
|
||||
\alias{fixedPanel}
|
||||
\title{Panel with absolute positioning}
|
||||
\usage{
|
||||
absolutePanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL, draggable = FALSE, fixed = FALSE,
|
||||
cursor = c("auto", "move", "default", "inherit"))
|
||||
absolutePanel(..., top = NULL, left = NULL, right = NULL,
|
||||
bottom = NULL, width = NULL, height = NULL, draggable = FALSE,
|
||||
fixed = FALSE, cursor = c("auto", "move", "default", "inherit"))
|
||||
|
||||
fixedPanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL, draggable = FALSE, cursor = c("auto",
|
||||
"move", "default", "inherit"))
|
||||
fixedPanel(..., top = NULL, left = NULL, right = NULL,
|
||||
bottom = NULL, width = NULL, height = NULL, draggable = FALSE,
|
||||
cursor = c("auto", "move", "default", "inherit"))
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Attributes (named arguments) or children (unnamed arguments) that
|
||||
|
||||
@@ -62,5 +62,7 @@ Other input elements: \code{\link{checkboxGroupInput}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}}
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}},
|
||||
\code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
\usage{
|
||||
bookmarkButton(label = "Bookmark...", icon = shiny::icon("link", lib =
|
||||
"glyphicon"),
|
||||
title = "Bookmark this application's state and get a URL for sharing.", ...,
|
||||
id = "._bookmark_")
|
||||
title = "Bookmark this application's state and get a URL for sharing.",
|
||||
..., id = "._bookmark_")
|
||||
}
|
||||
\arguments{
|
||||
\item{label}{The contents of the button or link--usually a text label, but
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
\alias{brushOpts}
|
||||
\title{Create an object representing brushing options}
|
||||
\usage{
|
||||
brushOpts(id = NULL, fill = "#9cf", stroke = "#036", opacity = 0.25,
|
||||
delay = 300, delayType = c("debounce", "throttle"), clip = TRUE,
|
||||
direction = c("xy", "x", "y"), resetOnNew = FALSE)
|
||||
brushOpts(id = NULL, fill = "#9cf", stroke = "#036",
|
||||
opacity = 0.25, delay = 300, delayType = c("debounce", "throttle"),
|
||||
clip = TRUE, direction = c("xy", "x", "y"), resetOnNew = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{id}{Input value name. For example, if the value is \code{"plot_brush"},
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
\title{Checkbox Group Input Control}
|
||||
\usage{
|
||||
checkboxGroupInput(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
|
||||
inline = FALSE, width = NULL, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -93,5 +94,7 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}}
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}},
|
||||
\code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -46,5 +46,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{passwordInput}}, \code{\link{radioButtons}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
38
man/createRenderFunction.Rd
Normal file
38
man/createRenderFunction.Rd
Normal file
@@ -0,0 +1,38 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/shinywrappers.R
|
||||
\name{createRenderFunction}
|
||||
\alias{createRenderFunction}
|
||||
\title{Implement render functions}
|
||||
\usage{
|
||||
createRenderFunction(func, transform = function(value, session, name,
|
||||
...) value, outputFunc = NULL, outputArgs = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{func}{A function without parameters, that returns user data. If the
|
||||
returned value is a promise, then the render function will proceed in async
|
||||
mode.}
|
||||
|
||||
\item{transform}{A function that takes four arguments: \code{value},
|
||||
\code{session}, \code{name}, and \code{...} (for future-proofing). This
|
||||
function will be invoked each time a value is returned from \code{func},
|
||||
and is responsible for changing the value into a JSON-ready value to be
|
||||
JSON-encoded and sent to the browser.}
|
||||
|
||||
\item{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.}
|
||||
|
||||
\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{
|
||||
Implement render functions
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
\usage{
|
||||
dateInput(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
format = "yyyy-mm-dd", startview = "month", weekstart = 0,
|
||||
language = "en", width = NULL)
|
||||
language = "en", width = NULL, autoclose = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -43,6 +43,9 @@ Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
|
||||
|
||||
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
see \code{\link{validateCssUnit}}.}
|
||||
|
||||
\item{autoclose}{Whether or not to close the datepicker immediately when a
|
||||
date is selected.}
|
||||
}
|
||||
\description{
|
||||
Creates a text input which, when clicked on, brings up a calendar that
|
||||
@@ -104,5 +107,7 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}}
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}},
|
||||
\code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
\title{Create date range input}
|
||||
\usage{
|
||||
dateRangeInput(inputId, label, start = NULL, end = NULL, min = NULL,
|
||||
max = NULL, format = "yyyy-mm-dd", startview = "month", weekstart = 0,
|
||||
language = "en", separator = " to ", width = NULL)
|
||||
max = NULL, format = "yyyy-mm-dd", startview = "month",
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL,
|
||||
autoclose = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -49,6 +50,9 @@ Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
|
||||
|
||||
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
see \code{\link{validateCssUnit}}.}
|
||||
|
||||
\item{autoclose}{Whether or not to close the datepicker immediately when a
|
||||
date is selected.}
|
||||
}
|
||||
\description{
|
||||
Creates a pair of text inputs which, when clicked on, bring up calendars that
|
||||
@@ -121,5 +125,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{passwordInput}}, \code{\link{radioButtons}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
\alias{throttle}
|
||||
\title{Slow down a reactive expression with debounce/throttle}
|
||||
\usage{
|
||||
debounce(r, millis, priority = 100, domain = getDefaultReactiveDomain())
|
||||
debounce(r, millis, priority = 100,
|
||||
domain = getDefaultReactiveDomain())
|
||||
|
||||
throttle(r, millis, priority = 100, domain = getDefaultReactiveDomain())
|
||||
throttle(r, millis, priority = 100,
|
||||
domain = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{r}{A reactive expression (that invalidates too often).}
|
||||
|
||||
239
man/diskCache.Rd
Normal file
239
man/diskCache.Rd
Normal file
@@ -0,0 +1,239 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-disk.R
|
||||
\name{diskCache}
|
||||
\alias{diskCache}
|
||||
\title{Create a disk cache object}
|
||||
\usage{
|
||||
diskCache(dir = NULL, max_size = 10 * 1024^2, max_age = Inf,
|
||||
max_n = Inf, evict = c("lru", "fifo"), destroy_on_finalize = FALSE,
|
||||
missing = key_missing(), exec_missing = FALSE, logfile = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{dir}{Directory to store files for the cache. If \code{NULL} (the
|
||||
default) it will create and use a temporary directory.}
|
||||
|
||||
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
|
||||
this size, cached objects will be removed according to the value of the
|
||||
\code{evict}. Use \code{Inf} for no size limit.}
|
||||
|
||||
\item{max_age}{Maximum age of files in cache before they are evicted, in
|
||||
seconds. Use \code{Inf} for no age limit.}
|
||||
|
||||
\item{max_n}{Maximum number of objects in the cache. If the number of objects
|
||||
exceeds this value, then cached objects will be removed according to the
|
||||
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
|
||||
|
||||
\item{evict}{The eviction policy to use to decide which objects are removed
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{destroy_on_finalize}{If \code{TRUE}, then when the DiskCache object is
|
||||
garbage collected, the cache directory and all objects inside of it will be
|
||||
deleted from disk. If \code{FALSE} (the default), it will do nothing when
|
||||
finalized.}
|
||||
|
||||
\item{missing}{A value to return or a function to execute when
|
||||
\code{get(key)} is called but the key is not present in the cache. The
|
||||
default is a \code{\link{key_missing}} object. If it is a function to
|
||||
execute, the function must take one argument (the key), and you must also
|
||||
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
cases for it to throw an error, although another option is to return a
|
||||
value. If a value is returned, that value will in turn be returned by
|
||||
\code{get()}. See section Missing keys for more information.}
|
||||
|
||||
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
|
||||
as a value to return when \code{get()} results in a cache miss. If
|
||||
\code{TRUE}, treat \code{missing} as a function to execute when
|
||||
\code{get()} results in a cache miss.}
|
||||
|
||||
\item{logfile}{An optional filename or connection object to where logging
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
A disk cache object is a key-value store that saves the values as files in a
|
||||
directory on disk. Objects can be stored and retrieved using the \code{get()}
|
||||
and \code{set()} methods. Objects are automatically pruned from the cache
|
||||
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
and \code{evict}.
|
||||
}
|
||||
\section{Missing Keys}{
|
||||
|
||||
|
||||
The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
when \code{get()} is called with a key that is not in the cache (a cache
|
||||
miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
present in the cache. You can test if the returned value represents a
|
||||
missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
If you want to throw an error on a cache miss, you can do so by providing a
|
||||
function for \code{missing} that takes one argument, the key, and also use
|
||||
\code{exec_missing=TRUE}.
|
||||
|
||||
When the cache is created, you can supply a value for \code{missing}, which
|
||||
sets the default value to be returned for missing values. It can also be
|
||||
overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
|
||||
If your cache is configured so that \code{get()} returns a sentinel value
|
||||
to represent a cache miss, then \code{set} will also not allow you to store
|
||||
the sentinel value in the cache. It will throw an error if you attempt to
|
||||
do so.
|
||||
|
||||
Instead of returning the same sentinel value each time there is cache miss,
|
||||
the cache can execute a function each time \code{get()} encounters missing
|
||||
key. If the function returns a value, then \code{get()} will in turn return
|
||||
that value. However, a more common use is for the function to throw an
|
||||
error. If an error is thrown, then \code{get()} will not return a value.
|
||||
|
||||
To do this, pass a one-argument function to \code{missing}, and use
|
||||
\code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
prints the missing key, you could do this:
|
||||
|
||||
\preformatted{
|
||||
diskCache(
|
||||
missing = function(key) {
|
||||
stop("Attempted to get missing key: ", key)
|
||||
},
|
||||
exec_missing = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
If you use this, the code that calls \code{get()} should be wrapped with
|
||||
\code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
}
|
||||
|
||||
\section{Cache pruning}{
|
||||
|
||||
|
||||
Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
manually by calling \code{prune()}.
|
||||
|
||||
The disk cache will throttle the pruning so that it does not happen on
|
||||
every call to \code{set()}, because the filesystem operations for checking
|
||||
the status of files can be slow. Instead, it will prune once in every 20
|
||||
calls to \code{set()}, or if at least 5 seconds have elapsed since the last
|
||||
prune occurred, whichever is first. These parameters are currently not
|
||||
customizable, but may be in the future.
|
||||
|
||||
When a pruning occurs, if there are any objects that are older than
|
||||
\code{max_age}, they will be removed.
|
||||
|
||||
The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
individually.
|
||||
|
||||
If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
will be removed from the cache according to the eviction policy, which is
|
||||
set with the \code{evict} parameter. Objects will be removed so that the
|
||||
number of items is \code{max_n}.
|
||||
|
||||
If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
objects will be removed from the cache. Objects will be removed from the
|
||||
cache so that the total size remains under \code{max_size}. Note that the
|
||||
size is calculated using the size of the files, not the size of disk space
|
||||
used by the files -- these two values can differ because of files are
|
||||
stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
then a file that is one byte in size will take 4096 bytes on disk.
|
||||
|
||||
Another time that objects can be removed from the cache is when
|
||||
\code{get()} is called. If the target object is older than \code{max_age},
|
||||
it will be removed and the cache will report it as a missing value.
|
||||
}
|
||||
|
||||
\section{Eviction policies}{
|
||||
|
||||
|
||||
If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
from the cache according to an eviction policy. The available eviction
|
||||
policies are:
|
||||
|
||||
\describe{
|
||||
\item{\code{"lru"}}{
|
||||
Least Recently Used. The least recently used objects will be removed.
|
||||
This uses the filesystem's mtime property. When "lru" is used, each
|
||||
\code{get()} is called, it will update the file's mtime.
|
||||
}
|
||||
\item{\code{"fifo"}}{
|
||||
First-in-first-out. The oldest objects will be removed.
|
||||
}
|
||||
}
|
||||
|
||||
Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
FAT) have poor mtime resolution. (atime is not used because support for
|
||||
atime is worse than mtime.)
|
||||
}
|
||||
|
||||
\section{Sharing among multiple processes}{
|
||||
|
||||
|
||||
The directory for a DiskCache can be shared among multiple R processes. To
|
||||
do this, each R process should have a DiskCache object that uses the same
|
||||
directory. Each DiskCache will do pruning independently of the others, so if
|
||||
they have different pruning parameters, then one DiskCache may remove cached
|
||||
objects before another DiskCache would do so.
|
||||
|
||||
Even though it is possible for multiple processes to share a DiskCache
|
||||
directory, this should not be done on networked file systems, because of
|
||||
slow performance of networked file systems can cause problems. If you need
|
||||
a high-performance shared cache, you can use one built on a database like
|
||||
Redis, SQLite, mySQL, or similar.
|
||||
|
||||
When multiple processes share a cache directory, there are some potential
|
||||
race conditions. For example, if your code calls \code{exists(key)} to check
|
||||
if an object is in the cache, and then call \code{get(key)}, the object may
|
||||
be removed from the cache in between those two calls, and \code{get(key)}
|
||||
will throw an error. Instead of calling the two functions, it is better to
|
||||
simply call \code{get(key)}, and use \code{tryCatch()} to handle the error
|
||||
that is thrown if the object is not in the cache. This effectively tests for
|
||||
existence and gets the object in one operation.
|
||||
|
||||
It is also possible for one processes to prune objects at the same time that
|
||||
another processes is trying to prune objects. If this happens, you may see
|
||||
a warning from \code{file.remove()} failing to remove a file that has
|
||||
already been deleted.
|
||||
}
|
||||
|
||||
\section{Methods}{
|
||||
|
||||
|
||||
A disk cache object has the following methods:
|
||||
|
||||
\describe{
|
||||
\item{\code{get(key, missing, exec_missing)}}{
|
||||
Returns the value associated with \code{key}. If the key is not in the
|
||||
cache, then it returns the value specified by \code{missing} or,
|
||||
\code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
executes \code{missing}. The function can throw an error or return the
|
||||
value. If either of these parameters are specified here, then they
|
||||
will override the defaults that were set when the DiskCache object was
|
||||
created. See section Missing Keys for more information.
|
||||
}
|
||||
\item{\code{set(key, value)}}{
|
||||
Stores the \code{key}-\code{value} pair in the cache.
|
||||
}
|
||||
\item{\code{exists(key)}}{
|
||||
Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
\code{FALSE}.
|
||||
}
|
||||
\item{\code{size()}}{
|
||||
Returns the number of items currently in the cache.
|
||||
}
|
||||
\item{\code{keys()}}{
|
||||
Returns a character vector of all keys currently in the cache.
|
||||
}
|
||||
\item{\code{reset()}}{
|
||||
Clears all objects from the cache.
|
||||
}
|
||||
\item{\code{destroy()}}{
|
||||
Clears all objects in the cache, and removes the cache directory from
|
||||
disk.
|
||||
}
|
||||
\item{\code{prune()}}{
|
||||
Prunes the cache, using the parameters specified by \code{max_size},
|
||||
\code{max_age}, \code{max_n}, and \code{evict}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
\alias{getDefaultReactiveDomain}
|
||||
\alias{withReactiveDomain}
|
||||
\alias{onReactiveDomainEnded}
|
||||
\alias{domains}
|
||||
\title{Reactive domains}
|
||||
\usage{
|
||||
getDefaultReactiveDomain()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
\name{downloadButton}
|
||||
\alias{downloadButton}
|
||||
\alias{downloadLink}
|
||||
\alias{downloadLink}
|
||||
\title{Create a download button or link}
|
||||
\usage{
|
||||
downloadButton(outputId, label = "Download", class = NULL, ...)
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{downloadHandler}
|
||||
\title{File Downloads}
|
||||
\usage{
|
||||
downloadHandler(filename, content, contentType = NA, outputArgs = list())
|
||||
downloadHandler(filename, content, contentType = NA,
|
||||
outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{filename}{A string of the filename, including extension, that the
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
\alias{fileInput}
|
||||
\title{File Upload Control}
|
||||
\usage{
|
||||
fileInput(inputId, label, multiple = FALSE, accept = NULL, width = NULL,
|
||||
buttonLabel = "Browse...", placeholder = "No file selected")
|
||||
fileInput(inputId, label, multiple = FALSE, accept = NULL,
|
||||
width = NULL, buttonLabel = "Browse...",
|
||||
placeholder = "No file selected")
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -97,5 +98,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{passwordInput}}, \code{\link{radioButtons}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{fillPage}
|
||||
\title{Create a page that fills the window}
|
||||
\usage{
|
||||
fillPage(..., padding = 0, title = NULL, bootstrap = TRUE, theme = NULL)
|
||||
fillPage(..., padding = 0, title = NULL, bootstrap = TRUE,
|
||||
theme = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Elements to include within the page.}
|
||||
|
||||
14
man/getCurrentOutputInfo.Rd
Normal file
14
man/getCurrentOutputInfo.Rd
Normal file
@@ -0,0 +1,14 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/shiny.R
|
||||
\name{getCurrentOutputInfo}
|
||||
\alias{getCurrentOutputInfo}
|
||||
\title{Get information about the output that is currently being executed.}
|
||||
\usage{
|
||||
getCurrentOutputInfo(session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The current Shiny session.}
|
||||
}
|
||||
\description{
|
||||
Get information about the output that is currently being executed.
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{hoverOpts}
|
||||
\title{Create an object representing hover options}
|
||||
\usage{
|
||||
hoverOpts(id = NULL, delay = 300, delayType = c("debounce", "throttle"),
|
||||
clip = TRUE, nullOutside = TRUE)
|
||||
hoverOpts(id = NULL, delay = 300, delayType = c("debounce",
|
||||
"throttle"), clip = TRUE, nullOutside = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{id}{Input value name. For example, if the value is \code{"plot_hover"},
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
\alias{uiOutput}
|
||||
\title{Create an HTML output element}
|
||||
\usage{
|
||||
htmlOutput(outputId, inline = FALSE, container = if (inline) span else div,
|
||||
...)
|
||||
htmlOutput(outputId, inline = FALSE, container = if (inline) span else
|
||||
div, ...)
|
||||
|
||||
uiOutput(outputId, inline = FALSE, container = if (inline) span else div,
|
||||
...)
|
||||
uiOutput(outputId, inline = FALSE, container = if (inline) span else
|
||||
div, ...)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the value from}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
\alias{installExprFunction}
|
||||
\title{Install an expression as a function}
|
||||
\usage{
|
||||
installExprFunction(expr, name, eval.env = parent.frame(2), quoted = FALSE,
|
||||
assign.env = parent.frame(1), label = deparse(sys.call(-1)[[1]]),
|
||||
wrappedWithLabel = TRUE, ..stacktraceon = FALSE)
|
||||
installExprFunction(expr, name, eval.env = parent.frame(2),
|
||||
quoted = FALSE, assign.env = parent.frame(1),
|
||||
label = deparse(sys.call(-1)[[1]]), wrappedWithLabel = TRUE,
|
||||
..stacktraceon = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{A quoted or unquoted expression}
|
||||
|
||||
20
man/key_missing.Rd
Normal file
20
man/key_missing.Rd
Normal file
@@ -0,0 +1,20 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-utils.R
|
||||
\name{key_missing}
|
||||
\alias{key_missing}
|
||||
\alias{is.key_missing}
|
||||
\title{A Key Missing object}
|
||||
\usage{
|
||||
key_missing()
|
||||
|
||||
is.key_missing(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{An object to test.}
|
||||
}
|
||||
\description{
|
||||
A \code{key_missing} object represents a cache miss.
|
||||
}
|
||||
\seealso{
|
||||
\code{\link{diskCache}}, \code{\link{memoryCache}}.
|
||||
}
|
||||
199
man/memoryCache.Rd
Normal file
199
man/memoryCache.Rd
Normal file
@@ -0,0 +1,199 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/cache-memory.R
|
||||
\name{memoryCache}
|
||||
\alias{memoryCache}
|
||||
\title{Create a memory cache object}
|
||||
\usage{
|
||||
memoryCache(max_size = 10 * 1024^2, max_age = Inf, max_n = Inf,
|
||||
evict = c("lru", "fifo"), missing = key_missing(),
|
||||
exec_missing = FALSE, logfile = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
|
||||
this size, cached objects will be removed according to the value of the
|
||||
\code{evict}. Use \code{Inf} for no size limit.}
|
||||
|
||||
\item{max_age}{Maximum age of files in cache before they are evicted, in
|
||||
seconds. Use \code{Inf} for no age limit.}
|
||||
|
||||
\item{max_n}{Maximum number of objects in the cache. If the number of objects
|
||||
exceeds this value, then cached objects will be removed according to the
|
||||
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
|
||||
|
||||
\item{evict}{The eviction policy to use to decide which objects are removed
|
||||
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
|
||||
supported.}
|
||||
|
||||
\item{missing}{A value to return or a function to execute when
|
||||
\code{get(key)} is called but the key is not present in the cache. The
|
||||
default is a \code{\link{key_missing}} object. If it is a function to
|
||||
execute, the function must take one argument (the key), and you must also
|
||||
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
|
||||
cases for it to throw an error, although another option is to return a
|
||||
value. If a value is returned, that value will in turn be returned by
|
||||
\code{get()}. See section Missing keys for more information.}
|
||||
|
||||
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
|
||||
as a value to return when \code{get()} results in a cache miss. If
|
||||
\code{TRUE}, treat \code{missing} as a function to execute when
|
||||
\code{get()} results in a cache miss.}
|
||||
|
||||
\item{logfile}{An optional filename or connection object to where logging
|
||||
information will be written. To log to the console, use \code{stdout()}.}
|
||||
}
|
||||
\description{
|
||||
A memory cache object is a key-value store that saves the values in an
|
||||
environment. Objects can be stored and retrieved using the \code{get()} and
|
||||
\code{set()} methods. Objects are automatically pruned from the cache
|
||||
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
|
||||
and \code{evict}.
|
||||
}
|
||||
\details{
|
||||
In a \code{MemoryCache}, R objects are stored directly in the cache; they are
|
||||
not \emph{not} serialized before being stored in the cache. This contrasts
|
||||
with other cache types, like \code{\link{diskCache}}, where objects are
|
||||
serialized, and the serialized object is cached. This can result in some
|
||||
differences of behavior. For example, as long as an object is stored in a
|
||||
MemoryCache, it will not be garbage collected.
|
||||
}
|
||||
\section{Missing keys}{
|
||||
|
||||
The \code{missing} and \code{exec_missing} parameters controls what happens
|
||||
when \code{get()} is called with a key that is not in the cache (a cache
|
||||
miss). The default behavior is to return a \code{\link{key_missing}}
|
||||
object. This is a \emph{sentinel value} that indicates that the key was not
|
||||
present in the cache. You can test if the returned value represents a
|
||||
missing key by using the \code{\link{is.key_missing}} function. You can
|
||||
also have \code{get()} return a different sentinel value, like \code{NULL}.
|
||||
If you want to throw an error on a cache miss, you can do so by providing a
|
||||
function for \code{missing} that takes one argument, the key, and also use
|
||||
\code{exec_missing=TRUE}.
|
||||
|
||||
When the cache is created, you can supply a value for \code{missing}, which
|
||||
sets the default value to be returned for missing values. It can also be
|
||||
overridden when \code{get()} is called, by supplying a \code{missing}
|
||||
argument. For example, if you use \code{cache$get("mykey", missing =
|
||||
NULL)}, it will return \code{NULL} if the key is not in the cache.
|
||||
|
||||
If your cache is configured so that \code{get()} returns a sentinel value
|
||||
to represent a cache miss, then \code{set} will also not allow you to store
|
||||
the sentinel value in the cache. It will throw an error if you attempt to
|
||||
do so.
|
||||
|
||||
Instead of returning the same sentinel value each time there is cache miss,
|
||||
the cache can execute a function each time \code{get()} encounters missing
|
||||
key. If the function returns a value, then \code{get()} will in turn return
|
||||
that value. However, a more common use is for the function to throw an
|
||||
error. If an error is thrown, then \code{get()} will not return a value.
|
||||
|
||||
To do this, pass a one-argument function to \code{missing}, and use
|
||||
\code{exec_missing=TRUE}. For example, if you want to throw an error that
|
||||
prints the missing key, you could do this:
|
||||
|
||||
\preformatted{
|
||||
diskCache(
|
||||
missing = function(key) {
|
||||
stop("Attempted to get missing key: ", key)
|
||||
},
|
||||
exec_missing = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
If you use this, the code that calls \code{get()} should be wrapped with
|
||||
\code{\link{tryCatch}()} to gracefully handle missing keys.
|
||||
}
|
||||
|
||||
\section{Cache pruning}{
|
||||
|
||||
|
||||
Cache pruning occurs when \code{set()} is called, or it can be invoked
|
||||
manually by calling \code{prune()}.
|
||||
|
||||
When a pruning occurs, if there are any objects that are older than
|
||||
\code{max_age}, they will be removed.
|
||||
|
||||
The \code{max_size} and \code{max_n} parameters are applied to the cache as
|
||||
a whole, in contrast to \code{max_age}, which is applied to each object
|
||||
individually.
|
||||
|
||||
If the number of objects in the cache exceeds \code{max_n}, then objects
|
||||
will be removed from the cache according to the eviction policy, which is
|
||||
set with the \code{evict} parameter. Objects will be removed so that the
|
||||
number of items is \code{max_n}.
|
||||
|
||||
If the size of the objects in the cache exceeds \code{max_size}, then
|
||||
objects will be removed from the cache. Objects will be removed from the
|
||||
cache so that the total size remains under \code{max_size}. Note that the
|
||||
size is calculated using the size of the files, not the size of disk space
|
||||
used by the files -- these two values can differ because of files are
|
||||
stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
then a file that is one byte in size will take 4096 bytes on disk.
|
||||
|
||||
Another time that objects can be removed from the cache is when
|
||||
\code{get()} is called. If the target object is older than \code{max_age},
|
||||
it will be removed and the cache will report it as a missing value.
|
||||
}
|
||||
|
||||
\section{Eviction policies}{
|
||||
|
||||
|
||||
If \code{max_n} or \code{max_size} are used, then objects will be removed
|
||||
from the cache according to an eviction policy. The available eviction
|
||||
policies are:
|
||||
|
||||
\describe{
|
||||
\item{\code{"lru"}}{
|
||||
Least Recently Used. The least recently used objects will be removed.
|
||||
This uses the filesystem's atime property. Some filesystems do not
|
||||
support atime, or have a very low atime resolution. The DiskCache will
|
||||
check for atime support, and if the filesystem does not support atime,
|
||||
a warning will be issued and the "fifo" policy will be used instead.
|
||||
}
|
||||
\item{\code{"fifo"}}{
|
||||
First-in-first-out. The oldest objects will be removed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\section{Methods}{
|
||||
|
||||
|
||||
A disk cache object has the following methods:
|
||||
|
||||
\describe{
|
||||
\item{\code{get(key, missing, exec_missing)}}{
|
||||
Returns the value associated with \code{key}. If the key is not in the
|
||||
cache, then it returns the value specified by \code{missing} or,
|
||||
\code{missing} is a function and \code{exec_missing=TRUE}, then
|
||||
executes \code{missing}. The function can throw an error or return the
|
||||
value. If either of these parameters are specified here, then they
|
||||
will override the defaults that were set when the DiskCache object was
|
||||
created. See section Missing Keys for more information.
|
||||
}
|
||||
\item{\code{set(key, value)}}{
|
||||
Stores the \code{key}-\code{value} pair in the cache.
|
||||
}
|
||||
\item{\code{exists(key)}}{
|
||||
Returns \code{TRUE} if the cache contains the key, otherwise
|
||||
\code{FALSE}.
|
||||
}
|
||||
\item{\code{size()}}{
|
||||
Returns the number of items currently in the cache.
|
||||
}
|
||||
\item{\code{keys()}}{
|
||||
Returns a character vector of all keys currently in the cache.
|
||||
}
|
||||
\item{\code{reset()}}{
|
||||
Clears all objects from the cache.
|
||||
}
|
||||
\item{\code{destroy()}}{
|
||||
Clears all objects in the cache, and removes the cache directory from
|
||||
disk.
|
||||
}
|
||||
\item{\code{prune()}}{
|
||||
Prunes the cache, using the parameters specified by \code{max_size},
|
||||
\code{max_age}, \code{max_n}, and \code{evict}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
\title{Create a page with a top level navigation bar}
|
||||
\usage{
|
||||
navbarPage(title, ..., id = NULL, selected = NULL,
|
||||
position = c("static-top", "fixed-top", "fixed-bottom"), header = NULL,
|
||||
footer = NULL, inverse = FALSE, collapsible = FALSE, collapsable,
|
||||
fluid = TRUE, responsive = NULL, theme = NULL, windowTitle = title)
|
||||
position = c("static-top", "fixed-top", "fixed-bottom"),
|
||||
header = NULL, footer = NULL, inverse = FALSE,
|
||||
collapsible = FALSE, collapsable, fluid = TRUE, responsive = NULL,
|
||||
theme = NULL, windowTitle = title)
|
||||
|
||||
navbarMenu(title, ..., menuName = title, icon = NULL)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{navlistPanel}
|
||||
\title{Create a navigation list panel}
|
||||
\usage{
|
||||
navlistPanel(..., id = NULL, selected = NULL, well = TRUE, fluid = TRUE,
|
||||
widths = c(4, 8))
|
||||
navlistPanel(..., id = NULL, selected = NULL, well = TRUE,
|
||||
fluid = TRUE, widths = c(4, 8))
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{\code{\link{tabPanel}} elements to include in the navlist}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
\title{Find rows of data that are near a click/hover/double-click}
|
||||
\usage{
|
||||
nearPoints(df, coordinfo, xvar = NULL, yvar = NULL, panelvar1 = NULL,
|
||||
panelvar2 = NULL, threshold = 5, maxpoints = NULL, addDist = FALSE,
|
||||
allRows = FALSE)
|
||||
panelvar2 = NULL, threshold = 5, maxpoints = NULL,
|
||||
addDist = FALSE, allRows = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{df}{A data frame from which to select rows.}
|
||||
|
||||
@@ -53,5 +53,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{passwordInput}}, \code{\link{radioButtons}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
\title{Create a reactive observer}
|
||||
\usage{
|
||||
observe(x, env = parent.frame(), quoted = FALSE, label = NULL,
|
||||
suspended = FALSE, priority = 0, domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE, ..stacktraceon = TRUE)
|
||||
suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{An expression (quoted or unquoted). Any return value will be
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
\usage{
|
||||
observeEvent(eventExpr, handlerExpr, event.env = parent.frame(),
|
||||
event.quoted = FALSE, handler.env = parent.frame(),
|
||||
handler.quoted = FALSE, label = NULL, suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
|
||||
handler.quoted = FALSE, label = NULL, suspended = FALSE,
|
||||
priority = 0, domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE, ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE)
|
||||
|
||||
eventReactive(eventExpr, valueExpr, event.env = parent.frame(),
|
||||
event.quoted = FALSE, value.env = parent.frame(), value.quoted = FALSE,
|
||||
label = NULL, domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
|
||||
ignoreInit = FALSE)
|
||||
event.quoted = FALSE, value.env = parent.frame(),
|
||||
value.quoted = FALSE, label = NULL,
|
||||
domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
|
||||
ignoreInit = FALSE, cache = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{eventExpr}{A (quoted or unquoted) expression that represents the event;
|
||||
@@ -82,6 +84,14 @@ this is the calling environment.}
|
||||
\item{value.quoted}{Is the \code{valueExpr} expression quoted? By default,
|
||||
this is \code{FALSE}. This is useful when you want to use an expression
|
||||
that is stored in a variable; to do so, it must be quoted with \code{quote()}.}
|
||||
|
||||
\item{cache}{Extra caching to use for \code{eventReactive}. Note that the
|
||||
most recent value is always cached, but this option allows you to cache
|
||||
previous values based on the value of \code{eventExpr}. If \code{NULL} (the
|
||||
default), do not use extra caching. Other possible values are \code{"app"}
|
||||
for an application-level cache, \code{"session"} for a session-level cache,
|
||||
or a cache object with \code{$get()} and \code{$set()} methods. See
|
||||
\code{\link{renderCachedPlot}} for more information about using caching.}
|
||||
}
|
||||
\value{
|
||||
\code{observeEvent} returns an observer reference class object (see
|
||||
@@ -135,15 +145,20 @@ 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).
|
||||
|
||||
Unlike what happens for \code{ignoreNULL}, only \code{observeEvent} takes in an
|
||||
\code{ignoreInit} argument. By default, \code{observeEvent} will run right when
|
||||
it is created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
|
||||
Likewise, both \code{observeEvent} and \code{eventReactive} also take in an
|
||||
\code{ignoreInit} argument. By default, both of these will run right when they
|
||||
are created (except if, at that moment, \code{eventExpr} evaluates to \code{NULL}
|
||||
and \code{ignoreNULL} is \code{TRUE}). But when responding to a click of an action
|
||||
button, it may often be useful to set \code{ignoreInit} to \code{TRUE}. For
|
||||
example, if you're setting up an \code{observeEvent} for a dynamically created
|
||||
button, then \code{ignoreInit = TRUE} will guarantee that the action (in
|
||||
\code{handlerExpr}) will only be triggered when the button is actually clicked,
|
||||
instead of also being triggered when it is created/initialized.
|
||||
instead of also being triggered when it is created/initialized. Similarly,
|
||||
if you're setting up an \code{eventReactive} that responds to a dynamically
|
||||
created button used to refresh some data (then returned by that \code{eventReactive}),
|
||||
then you should use \code{eventReactive([...], ignoreInit = TRUE)} if you want
|
||||
to let the user decide if/when they want to refresh the data (since, depending
|
||||
on the app, this may be a computationally expensive operation).
|
||||
|
||||
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
|
||||
@@ -151,29 +166,68 @@ these:
|
||||
|
||||
\describe{
|
||||
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
|
||||
This is the default. This combination means that \code{handlerExpr} will
|
||||
run every time that \code{eventExpr} is not \code{NULL}. If, at the time
|
||||
of the \code{observeEvent}'s creation, \code{handleExpr} happens to
|
||||
\emph{not} be \code{NULL}, then the code runs.
|
||||
This is the default. This combination means that \code{handlerExpr}/
|
||||
\code{valueExpr} will run every time that \code{eventExpr} is not
|
||||
\code{NULL}. If, at the time of the creation of the
|
||||
\code{observeEvent}/\code{eventReactive}, \code{eventExpr} happens
|
||||
to \emph{not} be \code{NULL}, then the code runs.
|
||||
}
|
||||
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
|
||||
This combination means that \code{handlerExpr} will run every time no
|
||||
matter what.
|
||||
This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
run every time no matter what.
|
||||
}
|
||||
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
|
||||
This combination means that \code{handlerExpr} will \emph{not} run when
|
||||
the \code{observeEvent} is created (because \code{ignoreInit = TRUE}),
|
||||
but it will run every other time.
|
||||
This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
\emph{not} run when the \code{observeEvent}/\code{eventReactive} is
|
||||
created (because \code{ignoreInit = TRUE}), but it will run every
|
||||
other time.
|
||||
}
|
||||
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
|
||||
This combination means that \code{handlerExpr} will \emph{not} run when
|
||||
the \code{observeEvent} is created (because \code{ignoreInit = TRUE}).
|
||||
After that, \code{handlerExpr} will run every time that \code{eventExpr}
|
||||
is not \code{NULL}.
|
||||
This combination means that \code{handlerExpr}/\code{valueExpr} will
|
||||
\emph{not} run when the \code{observeEvent}/\code{eventReactive} is
|
||||
created (because \code{ignoreInit = TRUE}). After that,
|
||||
\code{handlerExpr}/\code{valueExpr} will run every time that
|
||||
\code{eventExpr} is not \code{NULL}.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\section{\code{eventReactive} caching}{
|
||||
|
||||
|
||||
Like regular \code{\link{reactive}} expressions, the most recent value of a
|
||||
\code{eventReactive} is always cached. (Observers are not cached because
|
||||
they are used for their side-effects, not their values.) If a
|
||||
\code{reactive} or \code{eventReactive} named \code{r} is called with
|
||||
\code{r()} and then called again (without being invalidated in between),
|
||||
then the second call will simply return the most recent value.
|
||||
|
||||
An \code{eventReactive} allows for caching of previous values, by using the
|
||||
\code{cache} parameter. When this additional caching is used, a key-value
|
||||
store is used, where the result of the \code{eventExpr} is used as the key.
|
||||
More specifically, the result from the \code{eventExpr} is combined with
|
||||
the \code{eventReactive}'s \code{label} (which defaults to a string
|
||||
representation of the \code{expr} code), and they are serialized and hashed
|
||||
to generate the key.
|
||||
|
||||
When an additional cache is used, it allow for sharing cached values with
|
||||
other sessions. If you use \code{cache="session"}, then a separate cache
|
||||
will be used for each user session. If you use \code{cache="app"}, then the
|
||||
cache for the \code{eventReactive} will be shared across multiple client
|
||||
sessions accessing the same Shiny application -- because the \code{label}
|
||||
will (by default) be the same when the \code{expr} code is the same, an
|
||||
\code{eventReactive} in one session can share values with the corresponding
|
||||
\code{eventReactive} in another session. Whenever they have the same result
|
||||
for \code{eventExpr}, the value can be drawn from the cache instead of
|
||||
being recomputed.
|
||||
|
||||
Other types of caching are possible, by passing a cache object with
|
||||
\code{$get()} and \code{$set()} methods. It is possible to cache the values
|
||||
to disk, or in an external database, and have the cache persist across
|
||||
application restarts. See \code{\link{renderCachedPlot}} for more
|
||||
information about caching with Shiny.
|
||||
}
|
||||
|
||||
\examples{
|
||||
## Only run this example in interactive R sessions
|
||||
if (interactive()) {
|
||||
|
||||
@@ -13,7 +13,9 @@ onStop(fun, session = getDefaultReactiveDomain())
|
||||
called from within the server function, this will default to the current
|
||||
session, and the callback will be invoked when the current session ends. If
|
||||
\code{onStop} is called outside a server function, then the callback will
|
||||
be invoked with the application exits.}
|
||||
be invoked with the application exits. If \code{NULL}, it is the same as
|
||||
calling \code{onStop} outside of the server function, and the callback will
|
||||
be invoked when the application exits.}
|
||||
}
|
||||
\value{
|
||||
A function which, if invoked, will cancel the callback.
|
||||
|
||||
@@ -55,5 +55,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{radioButtons}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
\name{plotOutput}
|
||||
\alias{plotOutput}
|
||||
\alias{imageOutput}
|
||||
\alias{plotOutput}
|
||||
\title{Create an plot or image output element}
|
||||
\usage{
|
||||
imageOutput(outputId, width = "100\%", height = "400px", click = NULL,
|
||||
dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
|
||||
inline = FALSE)
|
||||
imageOutput(outputId, width = "100\%", height = "400px",
|
||||
click = NULL, dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL,
|
||||
hoverId = NULL, inline = FALSE)
|
||||
|
||||
plotOutput(outputId, width = "100\%", height = "400px", click = NULL,
|
||||
dblclick = NULL, hover = NULL, hoverDelay = NULL,
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
|
||||
inline = FALSE)
|
||||
hoverDelayType = NULL, brush = NULL, clickId = NULL,
|
||||
hoverId = NULL, inline = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the plot/image from.}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
\title{Create radio buttons}
|
||||
\usage{
|
||||
radioButtons(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
|
||||
inline = FALSE, width = NULL, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -109,5 +110,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{selectInput}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
309
man/renderCachedPlot.Rd
Normal file
309
man/renderCachedPlot.Rd
Normal file
@@ -0,0 +1,309 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/render-cached-plot.R
|
||||
\name{renderCachedPlot}
|
||||
\alias{renderCachedPlot}
|
||||
\title{Plot output with cached images}
|
||||
\usage{
|
||||
renderCachedPlot(expr, cacheKeyExpr, sizePolicy = sizeGrowthRatio(width =
|
||||
400, height = 400, growthRate = 1.2), res = 72, cache = "app", ...,
|
||||
outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that generates a plot.}
|
||||
|
||||
\item{cacheKeyExpr}{An expression that returns a cache key. This key should
|
||||
be a unique identifier for a plot: the assumption is that if the cache key
|
||||
is the same, then the plot will be the same.}
|
||||
|
||||
\item{sizePolicy}{A function that takes two arguments, \code{width} and
|
||||
\code{height}, and returns a list with \code{width} and \code{height}. The
|
||||
purpose is to round the actual pixel dimensions from the browser to some
|
||||
other dimensions, so that this will not generate and cache images of every
|
||||
possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
|
||||
information on the default sizing policy.}
|
||||
|
||||
\item{res}{The resolution of the PNG, in pixels per inch.}
|
||||
|
||||
\item{cache}{The scope of the cache, or a cache object. This can be
|
||||
\code{"app"} (the default), \code{"session"}, or a cache object like
|
||||
a \code{\link{diskCache}}. See the Cache Scoping section for more
|
||||
information.}
|
||||
|
||||
\item{...}{Arguments to be passed through to \code{\link[grDevices]{png}}.
|
||||
These can be used to set the width, height, background color, etc.}
|
||||
|
||||
\item{outputArgs}{A list of arguments to be passed through to the implicit
|
||||
call to \code{\link{plotOutput}} when \code{renderPlot} is used in an
|
||||
interactive R Markdown document.}
|
||||
}
|
||||
\description{
|
||||
Renders a reactive plot, with plot images cached to disk.
|
||||
}
|
||||
\details{
|
||||
\code{expr} is an expression that generates a plot, similar to that in
|
||||
\code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
|
||||
take reactive dependencies. It is re-executed only when the cache key
|
||||
changes.
|
||||
|
||||
\code{cacheKeyExpr} is an expression which, when evaluated, returns an object
|
||||
which will be serialized and hashed using the \code{\link[digest]{digest}}
|
||||
function to generate a string that will be used as a cache key. This key is
|
||||
used to identify the contents of the plot: if the cache key is the same as a
|
||||
previous time, it assumes that the plot is the same and can be retrieved from
|
||||
the cache.
|
||||
|
||||
This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
|
||||
upstream reactives are invalidated. This will also trigger re-execution of
|
||||
the plotting expression, \code{expr}.
|
||||
|
||||
The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
should in turn contain other normal R objects. If the key contains
|
||||
environments, external pointers, or reference objects -- or even if it has
|
||||
such objects attached as attributes -- then it is possible that it will
|
||||
change unpredictably even when you do not expect it to. Additionally, because
|
||||
the entire key is serialized and hashed, if it contains a very large object
|
||||
-- a large data set, for example -- there may be a noticeable performance
|
||||
penalty.
|
||||
|
||||
If you face these issues with the cache key, you can work around them by
|
||||
extracting out the important parts of the objects, and/or by converting them
|
||||
to normal R objects before returning them. Your expression could even
|
||||
serialize and hash that information in an efficient way and return a string,
|
||||
which will in turn be hashed (very quickly) by the
|
||||
\code{\link[digest]{digest}} function.
|
||||
|
||||
Internally, the result from \code{cacheKeyExpr} is combined with the name of
|
||||
the output (if you assign it to \code{output$plot1}, it will be combined
|
||||
with \code{"plot1"}) to form the actual key that is used. As a result, even
|
||||
if there are multiple plots that have the same \code{cacheKeyExpr}, they
|
||||
will not have cache key collisions.
|
||||
}
|
||||
\section{Cache scoping}{
|
||||
|
||||
|
||||
There are a number of different ways you may want to scope the cache. For
|
||||
example, you may want each user session to have their own plot cache, or
|
||||
you may want each run of the application to have a cache (shared among
|
||||
possibly multiple simultaneous user sessions), or you may want to have a
|
||||
cache that persists even after the application is shut down and started
|
||||
again.
|
||||
|
||||
To control the scope of the cache, use the \code{cache} parameter. There
|
||||
are two ways of having Shiny automatically create and clean up the disk
|
||||
cache.
|
||||
|
||||
\describe{
|
||||
\item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
among possibly multiple user sessions), use \code{cache="app"}. This
|
||||
is the default. The cache will be shared across multiple sessions, so
|
||||
there is potentially a large performance benefit if there are many users
|
||||
of the application. When the application stops running, the cache will
|
||||
be deleted. If plots cannot be safely shared across users, this should
|
||||
not be used.}
|
||||
\item{2}{To scope the cache to one session, use \code{cache="session"}.
|
||||
When a new user session starts -- in other words, when a web browser
|
||||
visits the Shiny application -- a new cache will be created on disk
|
||||
for that session. When the session ends, the cache will be deleted.
|
||||
The cache will not be shared across multiple sessions.}
|
||||
}
|
||||
|
||||
If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
|
||||
in size, and will be stored stored in memory, using a
|
||||
\code{\link{memoryCache}} object. Note that the cache space will be shared
|
||||
among all cached plots within a single application or session.
|
||||
|
||||
In some cases, you may want more control over the caching behavior. For
|
||||
example, you may want to use a larger or smaller cache, share a cache
|
||||
among multiple R processes, or you may want the cache to persist across
|
||||
multiple runs of an application, or even across multiple R processes.
|
||||
|
||||
To use different settings for an application-scoped cache, you can call
|
||||
\code{\link{shinyOptions}()} at the top of your app.R, server.R, or
|
||||
global.R. For example, this will create a cache with 20 MB of space
|
||||
instead of the default 10 MB:
|
||||
\preformatted{
|
||||
shinyOptions(cache = memoryCache(size = 20e6))
|
||||
}
|
||||
|
||||
To use different settings for a session-scoped cache, you can call
|
||||
\code{\link{shinyOptions}()} at the top of your server function. To use
|
||||
the session-scoped cache, you must also call \code{renderCachedPlot} with
|
||||
\code{cache="session"}. This will create a 20 MB cache for the session:
|
||||
\preformatted{
|
||||
function(input, output, session) {
|
||||
shinyOptions(cache = memoryCache(size = 20e6))
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
...,
|
||||
cache = "session"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
If you want to create a cache that is shared across multiple concurrent
|
||||
R processes, you can use a \code{\link{diskCache}}. You can create an
|
||||
application-level shared cache by putting this at the top of your app.R,
|
||||
server.R, or global.R:
|
||||
\preformatted{
|
||||
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
}
|
||||
|
||||
This will create a subdirectory in your system temp directory named
|
||||
\code{myapp-cache} (replace \code{myapp-cache} with a unique name of
|
||||
your choosing). On most platforms, this directory will be removed when
|
||||
your system reboots. This cache will persist across multiple starts and
|
||||
stops of the R process, as long as you do not reboot.
|
||||
|
||||
To have the cache persist even across multiple reboots, you can create the
|
||||
cache in a location outside of the temp directory. For example, it could
|
||||
be a subdirectory of the application:
|
||||
\preformatted{
|
||||
shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
}
|
||||
|
||||
In this case, resetting the cache will have to be done manually, by deleting
|
||||
the directory.
|
||||
|
||||
You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
|
||||
as the \code{cache} argument of \code{renderCachedPlot}.
|
||||
}
|
||||
|
||||
\examples{
|
||||
## Only run examples in interactive R sessions
|
||||
if (interactive()) {
|
||||
|
||||
# A basic example that uses the default app-scoped memory cache.
|
||||
# The cache will be shared among all simultaneous users of the application.
|
||||
shinyApp(
|
||||
fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
),
|
||||
mainPanel(plotOutput("plot"))
|
||||
)
|
||||
),
|
||||
function(input, output, session) {
|
||||
output$plot <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
# An example uses a data object shared across sessions. mydata() is part of
|
||||
# the cache key, so when its value changes, plots that were previously
|
||||
# stored in the cache will no longer be used (unless mydata() changes back
|
||||
# to its previous value).
|
||||
mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
|
||||
ui <- fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
actionButton("newdata", "New data")
|
||||
),
|
||||
mainPanel(
|
||||
plotOutput("plot")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
server <- function(input, output, session) {
|
||||
observeEvent(input$newdata, {
|
||||
mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
})
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
{
|
||||
Sys.sleep(2)
|
||||
d <- mydata()
|
||||
seqn <- seq_len(input$n)
|
||||
plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n, mydata()) },
|
||||
)
|
||||
}
|
||||
|
||||
shinyApp(ui, server)
|
||||
|
||||
|
||||
# A basic application with two plots, where each plot in each session has
|
||||
# a separate cache.
|
||||
shinyApp(
|
||||
fluidPage(
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
),
|
||||
mainPanel(
|
||||
plotOutput("plot1"),
|
||||
plotOutput("plot2")
|
||||
)
|
||||
)
|
||||
),
|
||||
function(input, output, session) {
|
||||
output$plot1 <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = memoryCache()
|
||||
)
|
||||
output$plot2 <- renderCachedPlot({
|
||||
Sys.sleep(2) # Add an artificial delay
|
||||
seqn <- seq_len(input$n)
|
||||
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
},
|
||||
cacheKeyExpr = { list(input$n) },
|
||||
cache = memoryCache()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
\dontrun{
|
||||
# At the top of app.R, this set the application-scoped cache to be a memory
|
||||
# cache that is 20 MB in size, and where cached objects expire after one
|
||||
# hour.
|
||||
shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
|
||||
# At the top of app.R, this set the application-scoped cache to be a disk
|
||||
# cache that can be shared among multiple concurrent R processes, and is
|
||||
# deleted when the system reboots.
|
||||
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
|
||||
# At the top of app.R, this set the application-scoped cache to be a disk
|
||||
# cache that can be shared among multiple concurrent R processes, and
|
||||
# persists on disk across reboots.
|
||||
shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
|
||||
# At the top of the server function, this set the session-scoped cache to be
|
||||
# a memory cache that is 5 MB in size.
|
||||
server <- function(input, output, session) {
|
||||
shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
|
||||
output$plot <- renderCachedPlot(
|
||||
...,
|
||||
cache = "session"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
See \code{\link{renderPlot}} for the regular, non-cached version of
|
||||
this function. For more about configuring caches, see
|
||||
\code{\link{memoryCache}} and \code{\link{diskCache}}.
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
\title{Table output with the JavaScript library DataTables}
|
||||
\usage{
|
||||
renderDataTable(expr, options = NULL, searchDelay = 500,
|
||||
callback = "function(oTable) {}", escape = TRUE, env = parent.frame(),
|
||||
quoted = FALSE, outputArgs = list())
|
||||
callback = "function(oTable) {}", escape = TRUE,
|
||||
env = parent.frame(), quoted = FALSE, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a data frame or a matrix.}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{renderImage}
|
||||
\title{Image file output}
|
||||
\usage{
|
||||
renderImage(expr, env = parent.frame(), quoted = FALSE, deleteFile = TRUE,
|
||||
outputArgs = list())
|
||||
renderImage(expr, env = parent.frame(), quoted = FALSE,
|
||||
deleteFile = TRUE, outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a list.}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{renderUI}
|
||||
\title{UI Output}
|
||||
\usage{
|
||||
renderUI(expr, env = parent.frame(), quoted = FALSE, outputArgs = list())
|
||||
renderUI(expr, env = parent.frame(), quoted = FALSE,
|
||||
outputArgs = list())
|
||||
}
|
||||
\arguments{
|
||||
\item{expr}{An expression that returns a Shiny tag object, \code{\link{HTML}},
|
||||
@@ -20,8 +21,7 @@ call to \code{\link{uiOutput}} when \code{renderUI} is used in an
|
||||
interactive R Markdown document.}
|
||||
}
|
||||
\description{
|
||||
\bold{Experimental feature.} Makes a reactive version of a function that
|
||||
generates HTML using the Shiny UI library.
|
||||
Renders reactive HTML using the Shiny UI library.
|
||||
}
|
||||
\details{
|
||||
The corresponding HTML output tag should be \code{div} and have the CSS class
|
||||
@@ -48,5 +48,5 @@ shinyApp(ui, server)
|
||||
|
||||
}
|
||||
\seealso{
|
||||
conditionalPanel
|
||||
\code{\link{uiOutput}}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ shinyApp(
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
selectInput("state", "Choose a state:",
|
||||
list(`East Coast` = c("NY", "NJ", "CT"),
|
||||
`West Coast` = c("WA", "OR", "CA"),
|
||||
`Midwest` = c("MN", "WI", "IA"))
|
||||
list(`East Coast` = list("NY", "NJ", "CT"),
|
||||
`West Coast` = list("WA", "OR", "CA"),
|
||||
`Midwest` = list("MN", "WI", "IA"))
|
||||
),
|
||||
textOutput("result")
|
||||
),
|
||||
@@ -113,7 +113,7 @@ shinyApp(
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link{updateSelectInput}}
|
||||
\code{\link{updateSelectInput}} \code{\link{varSelectInput}}
|
||||
|
||||
Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{checkboxGroupInput}},
|
||||
@@ -122,5 +122,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{sliderInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -127,7 +127,8 @@
|
||||
Similar to \code{sendCustomMessage}, but the message must be a raw vector
|
||||
and the registration method on the client is
|
||||
\code{Shiny.addBinaryMessageHandler(type, function(message){...})}. The
|
||||
message argument on the client will be a \href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
|
||||
message argument on the client will be a
|
||||
\href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
|
||||
}
|
||||
\item{sendInputMessage(inputId, message)}{
|
||||
Sends a message to an input on the session's client web page; if the input
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
\alias{as.tags.shiny.appobj}
|
||||
\title{Create a Shiny app object}
|
||||
\usage{
|
||||
shinyApp(ui = NULL, server = NULL, onStart = NULL, options = list(),
|
||||
uiPattern = "/", enableBookmarking = NULL)
|
||||
shinyApp(ui = NULL, server = NULL, onStart = NULL,
|
||||
options = list(), uiPattern = "/", enableBookmarking = NULL)
|
||||
|
||||
shinyAppDir(appDir, options = list())
|
||||
|
||||
|
||||
33
man/sizeGrowthRatio.Rd
Normal file
33
man/sizeGrowthRatio.Rd
Normal file
@@ -0,0 +1,33 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/render-cached-plot.R
|
||||
\name{sizeGrowthRatio}
|
||||
\alias{sizeGrowthRatio}
|
||||
\title{Create a sizing function that grows at a given ratio}
|
||||
\usage{
|
||||
sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
}
|
||||
\arguments{
|
||||
\item{width, height}{Base width and height.}
|
||||
|
||||
\item{growthRate}{Growth rate multiplier.}
|
||||
}
|
||||
\description{
|
||||
Returns a function which takes a two-element vector representing an input
|
||||
width and height, and returns a two-element vector of width and height. The
|
||||
possible widths are the base width times the growthRate to any integer power.
|
||||
For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
widths.
|
||||
}
|
||||
\examples{
|
||||
f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
f(c(400, 400))
|
||||
f(c(500, 500))
|
||||
f(c(530, 550))
|
||||
f(c(625, 700))
|
||||
|
||||
}
|
||||
\seealso{
|
||||
This is to be used with \code{\link{renderCachedPlot}}.
|
||||
}
|
||||
@@ -5,10 +5,11 @@
|
||||
\alias{animationOptions}
|
||||
\title{Slider Input Widget}
|
||||
\usage{
|
||||
sliderInput(inputId, label, min, max, value, step = NULL, round = FALSE,
|
||||
format = NULL, locale = NULL, ticks = TRUE, animate = FALSE,
|
||||
width = NULL, sep = ",", pre = NULL, post = NULL, timeFormat = NULL,
|
||||
timezone = NULL, dragRange = TRUE)
|
||||
sliderInput(inputId, label, min, max, value, step = NULL,
|
||||
round = FALSE, format = NULL, locale = NULL, ticks = TRUE,
|
||||
animate = FALSE, width = NULL, sep = ",", pre = NULL,
|
||||
post = NULL, timeFormat = NULL, timezone = NULL,
|
||||
dragRange = TRUE)
|
||||
|
||||
animationOptions(interval = 1000, loop = FALSE, playButton = NULL,
|
||||
pauseButton = NULL)
|
||||
@@ -125,5 +126,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{submitButton}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{snapshotPreprocessInput}
|
||||
\title{Add a function for preprocessing an input before taking a test snapshot}
|
||||
\usage{
|
||||
snapshotPreprocessInput(inputId, fun, session = getDefaultReactiveDomain())
|
||||
snapshotPreprocessInput(inputId, fun,
|
||||
session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{Name of the input value.}
|
||||
|
||||
@@ -99,10 +99,12 @@ manipulating stack traces.
|
||||
from \code{conditionStackTrace(cond)}) and returns a data frame with one
|
||||
row for each stack frame and the columns \code{num} (stack frame number),
|
||||
\code{call} (a function name or similar), and \code{loc} (source file path
|
||||
and line number, if available).
|
||||
and line number, if available). It was deprecated after shiny 1.0.5 because
|
||||
it doesn't support deep stack traces.
|
||||
|
||||
\code{formatStackTrace} is similar to \code{extractStackTrace}, but
|
||||
it returns a preformatted character vector instead of a data frame.
|
||||
it returns a preformatted character vector instead of a data frame. It was
|
||||
deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
|
||||
|
||||
\code{conditionStackTrace} and \code{conditionStackTrace<-} are
|
||||
accessor functions for getting/setting stack traces on conditions.
|
||||
|
||||
@@ -72,5 +72,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{textAreaInput}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{tabsetPanel}
|
||||
\title{Create a tabset panel}
|
||||
\usage{
|
||||
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs", "pills"),
|
||||
position = NULL)
|
||||
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs",
|
||||
"pills"), position = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{\code{\link{tabPanel}} elements to include in the tabset}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
\alias{textAreaInput}
|
||||
\title{Create a textarea input control}
|
||||
\usage{
|
||||
textAreaInput(inputId, label, value = "", width = NULL, height = NULL,
|
||||
cols = NULL, rows = NULL, placeholder = NULL, resize = NULL)
|
||||
textAreaInput(inputId, label, value = "", width = NULL,
|
||||
height = NULL, cols = NULL, rows = NULL, placeholder = NULL,
|
||||
resize = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -68,5 +69,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textInput}}
|
||||
\code{\link{textInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{textInput}
|
||||
\title{Create a text input control}
|
||||
\usage{
|
||||
textInput(inputId, label, value = "", width = NULL, placeholder = NULL)
|
||||
textInput(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -50,5 +51,6 @@ Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textAreaInput}}
|
||||
\code{\link{textAreaInput}}, \code{\link{varSelectInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
\alias{textOutput}
|
||||
\title{Create a text output element}
|
||||
\usage{
|
||||
textOutput(outputId, container = if (inline) span else div, inline = FALSE)
|
||||
textOutput(outputId, container = if (inline) span else div,
|
||||
inline = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{outputId}{output variable to read the value from}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
\alias{updateCheckboxGroupInput}
|
||||
\title{Change the value of a checkbox group input on the client}
|
||||
\usage{
|
||||
updateCheckboxGroupInput(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE, choiceNames = NULL,
|
||||
choiceValues = NULL)
|
||||
updateCheckboxGroupInput(session, inputId, label = NULL,
|
||||
choices = NULL, selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
\alias{updateDateInput}
|
||||
\title{Change the value of a date input on the client}
|
||||
\usage{
|
||||
updateDateInput(session, inputId, label = NULL, value = NULL, min = NULL,
|
||||
max = NULL)
|
||||
updateDateInput(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
\name{updateSelectInput}
|
||||
\alias{updateSelectInput}
|
||||
\alias{updateSelectizeInput}
|
||||
\alias{updateVarSelectInput}
|
||||
\alias{updateVarSelectizeInput}
|
||||
\title{Change the value of a select input on the client}
|
||||
\usage{
|
||||
updateSelectInput(session, inputId, label = NULL, choices = NULL,
|
||||
@@ -10,6 +12,12 @@ updateSelectInput(session, inputId, label = NULL, choices = NULL,
|
||||
|
||||
updateSelectizeInput(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, options = list(), server = FALSE)
|
||||
|
||||
updateVarSelectInput(session, inputId, label = NULL, data = NULL,
|
||||
selected = NULL)
|
||||
|
||||
updateVarSelectizeInput(session, inputId, label = NULL, data = NULL,
|
||||
selected = NULL, options = list(), server = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
@@ -40,6 +48,8 @@ for details).}
|
||||
the select options dynamically on searching, instead of writing all
|
||||
\code{choices} into the page at once (i.e., only use the client-side
|
||||
version of \pkg{selectize.js})}
|
||||
|
||||
\item{data}{A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}}
|
||||
}
|
||||
\description{
|
||||
Change the value of a select input on the client
|
||||
@@ -94,5 +104,5 @@ shinyApp(ui, server)
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link{selectInput}}
|
||||
\code{\link{selectInput}} \code{\link{varSelectInput}}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
% Please edit documentation in R/update-input.R
|
||||
\name{updateSliderInput}
|
||||
\alias{updateSliderInput}
|
||||
\title{Change the value of a slider input on the client}
|
||||
\title{Update Slider Input Widget}
|
||||
\usage{
|
||||
updateSliderInput(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL)
|
||||
min = NULL, max = NULL, step = NULL, timeFormat = NULL,
|
||||
timezone = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{session}{The \code{session} object passed to function given to
|
||||
@@ -22,9 +23,13 @@ updateSliderInput(session, inputId, label = NULL, value = NULL,
|
||||
\item{max}{Maximum value.}
|
||||
|
||||
\item{step}{Step size.}
|
||||
|
||||
\item{timeFormat}{Date and POSIXt formatting.}
|
||||
|
||||
\item{timezone}{The timezone offset for POSIXt objects.}
|
||||
}
|
||||
\description{
|
||||
Change the value of a slider input on the client
|
||||
Change the value of a slider input on the client.
|
||||
}
|
||||
\details{
|
||||
The input updater functions send a message to the client, telling it to
|
||||
|
||||
129
man/varSelectInput.Rd
Normal file
129
man/varSelectInput.Rd
Normal file
@@ -0,0 +1,129 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/input-select.R
|
||||
\name{varSelectInput}
|
||||
\alias{varSelectInput}
|
||||
\alias{varSelectizeInput}
|
||||
\title{Select variables from a data frame}
|
||||
\usage{
|
||||
varSelectInput(inputId, label, data, selected = NULL, multiple = FALSE,
|
||||
selectize = TRUE, width = NULL, size = NULL)
|
||||
|
||||
varSelectizeInput(inputId, ..., options = NULL, width = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
|
||||
\item{label}{Display label for the control, or \code{NULL} for no label.}
|
||||
|
||||
\item{data}{A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}}
|
||||
|
||||
\item{selected}{The initially selected value (or multiple values if
|
||||
\code{multiple = TRUE}). If not specified then defaults to the first value
|
||||
for single-select lists and no values for multiple select lists.}
|
||||
|
||||
\item{multiple}{Is selection of multiple items allowed?}
|
||||
|
||||
\item{selectize}{Whether to use \pkg{selectize.js} or not.}
|
||||
|
||||
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
see \code{\link{validateCssUnit}}.}
|
||||
|
||||
\item{size}{Number of items to show in the selection box; a larger number
|
||||
will result in a taller box. Not compatible with \code{selectize=TRUE}.
|
||||
Normally, when \code{multiple=FALSE}, a select input will be a drop-down
|
||||
list, but when \code{size} is set, it will be a box instead.}
|
||||
|
||||
\item{...}{Arguments passed to \code{varSelectInput()}.}
|
||||
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}
|
||||
for possible options (character option values inside \code{\link[base]{I}()} will
|
||||
be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
|
||||
for details).}
|
||||
}
|
||||
\value{
|
||||
A variable select list control that can be added to a UI definition.
|
||||
}
|
||||
\description{
|
||||
Create a select list that can be used to choose a single or multiple items
|
||||
from the column names of a data frame.
|
||||
}
|
||||
\details{
|
||||
The resulting server \code{input} value will be returned as:
|
||||
\itemize{
|
||||
\item a symbol if \code{multiple = FALSE}. The \code{input} value should be
|
||||
used with rlang's \code{\link[rlang]{!!}}. For example,
|
||||
\code{ggplot2::aes(!!input$variable)}.
|
||||
\item a list of symbols if \code{multiple = TRUE}. The \code{input} value
|
||||
should be used with rlang's \code{\link[rlang]{!!!}} to expand
|
||||
the symbol list as individual arguments. For example,
|
||||
\code{dplyr::select(mtcars, !!!input$variabls)} which is
|
||||
equivalent to \code{dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])}.
|
||||
}
|
||||
|
||||
By default, \code{varSelectInput()} and \code{selectizeInput()} use the
|
||||
JavaScript library \pkg{selectize.js}
|
||||
(\url{https://github.com/selectize/selectize.js}) to instead of the basic
|
||||
select input element. To use the standard HTML select input element, use
|
||||
\code{selectInput()} with \code{selectize=FALSE}.
|
||||
}
|
||||
\note{
|
||||
The variable selectize input created from \code{varSelectizeInput()} allows
|
||||
deletion of the selected option even in a single select input, which will
|
||||
return an empty string as its value. This is the default behavior of
|
||||
\pkg{selectize.js}. However, the selectize input created from
|
||||
\code{selectInput(..., selectize = TRUE)} will ignore the empty string
|
||||
value when it is a single choice input and the empty string is not in the
|
||||
\code{choices} argument. This is to keep compatibility with
|
||||
\code{selectInput(..., selectize = FALSE)}.
|
||||
}
|
||||
\examples{
|
||||
|
||||
## Only run examples in interactive R sessions
|
||||
if (interactive()) {
|
||||
|
||||
library(ggplot2)
|
||||
|
||||
# single selection
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
varSelectInput("variable", "Variable:", mtcars),
|
||||
plotOutput("data")
|
||||
),
|
||||
server = function(input, output) {
|
||||
output$data <- renderPlot({
|
||||
ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# multiple selections
|
||||
\dontrun{
|
||||
shinyApp(
|
||||
ui = fluidPage(
|
||||
varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
|
||||
tableOutput("data")
|
||||
),
|
||||
server = function(input, output) {
|
||||
output$data <- renderTable({
|
||||
if (length(input$variables) == 0) return(mtcars)
|
||||
mtcars \%>\% dplyr::select(!!!input$variables)
|
||||
}, rownames = TRUE)
|
||||
}
|
||||
)}
|
||||
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link{updateSelectInput}}
|
||||
|
||||
Other input elements: \code{\link{actionButton}},
|
||||
\code{\link{checkboxGroupInput}},
|
||||
\code{\link{checkboxInput}}, \code{\link{dateInput}},
|
||||
\code{\link{dateRangeInput}}, \code{\link{fileInput}},
|
||||
\code{\link{numericInput}}, \code{\link{passwordInput}},
|
||||
\code{\link{radioButtons}}, \code{\link{selectInput}},
|
||||
\code{\link{sliderInput}}, \code{\link{submitButton}},
|
||||
\code{\link{textAreaInput}}, \code{\link{textInput}}
|
||||
}
|
||||
\concept{input elements}
|
||||
@@ -7,9 +7,10 @@
|
||||
\title{Reporting progress (functional API)}
|
||||
\usage{
|
||||
withProgress(expr, min = 0, max = 1, value = min + (max - min) * 0.1,
|
||||
message = NULL, detail = NULL, style = getShinyOption("progress.style",
|
||||
default = "notification"), session = getDefaultReactiveDomain(),
|
||||
env = parent.frame(), quoted = FALSE)
|
||||
message = NULL, detail = NULL,
|
||||
style = getShinyOption("progress.style", default = "notification"),
|
||||
session = getDefaultReactiveDomain(), env = parent.frame(),
|
||||
quoted = FALSE)
|
||||
|
||||
setProgress(value = NULL, message = NULL, detail = NULL,
|
||||
session = getDefaultReactiveDomain())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user