Compare commits

...

156 Commits

Author SHA1 Message Date
Winston Chang
98d4b5e487 Check for existence of bookmark scope directory before creating 2018-08-24 12:43:45 -05:00
Winston Chang
32913f9d95 Merge pull request #2160 from rstudio/digest-xxhash
Use xxhash64 instead of sha256 for hash algorithm
2018-08-17 11:59:47 -05:00
Winston Chang
cbabf9a2a3 Use xxhash64 instead of sha256 for hash algorithm 2018-08-16 15:54:54 -05:00
Winston Chang
03e92c3336 Update NEWS 2018-08-10 21:18:05 -05:00
Winston Chang
997c39fdc0 Merge pull request #2125 from rstudio/plot-interact-scaled
Fix plot interaction for scaled plots
2018-08-10 21:13:16 -05:00
Winston Chang
bba2d1ee18 Grunt 2018-08-10 19:42:11 -05:00
Winston Chang
a60301810f Update coordmap tests 2018-08-10 19:42:11 -05:00
Winston Chang
6b261f76b1 Bump version and update NEWS 2018-08-07 15:25:31 -05:00
Winston Chang
3db5f21d90 Update data structure comment 2018-08-07 15:11:43 -05:00
Winston Chang
121bfcb984 Import old brush after image has loaded 2018-08-07 15:11:43 -05:00
Winston Chang
265de66946 Make sure not to have multiple reset event handlers 2018-08-07 14:51:23 -05:00
Winston Chang
79c5c9f95e Add isnan() function for IE 2018-08-07 14:51:23 -05:00
Winston Chang
3354a47e8a Add width/height to coordmap instead of using naturalWidth/Height
This eliminates the need to use an on load callback.
2018-08-07 14:51:23 -05:00
Winston Chang
a1e1416d7a More consistent use of img to css conversion functions 2018-08-07 10:48:42 -05:00
Winston Chang
24b7a9907f renderCachedPlot: add note about interactive plots to help page 2018-08-07 10:48:42 -05:00
Joe Cheng
0bb53e8ca5 Inputs in renderUI/uiOutput don't work with bookmarks (#2139)
* hasCurrentRestoreContext returns FALSE from server side

Fixes #2138.

* Add NEWS item for renderUI bookmarking fix
2018-08-06 15:04:16 -07:00
Winston Chang
ec12caaeba Include x and y pixelratio in coordinfo 2018-08-06 12:51:08 -05:00
Winston Chang
5bbf2aa57a Use canonical CSS property name
Firefox doesn't support shorthand properties like "border-left", but instead
requires "border-left-width".
2018-08-06 12:51:08 -05:00
Winston Chang
84ad9997da Reposition div when resized (without new image) 2018-08-06 12:51:08 -05:00
Winston Chang
9f6ce87443 Remove redundant isEquivalent function 2018-08-06 12:51:08 -05:00
Winston Chang
1ff6c382bf Remove unnecessary ggplot2 workaround 2018-08-06 12:51:07 -05:00
Winston Chang
c366c10ae1 Initialize coordmap only after image loads 2018-08-06 12:51:07 -05:00
Winston Chang
950df1e25c Add support for scaled images and brushing 2018-08-06 12:51:07 -05:00
Winston Chang
909bfa8c14 Allow plot interaction to handle scaled images 2018-08-06 12:51:07 -05:00
Winston Chang
598b48d078 DiskCache: use mtime instead of atime, check for time resolution (#2146)
* DiskCache: check for atime support while running

* Use mtime instead of atime

* Remove mtime resolution checks
2018-08-06 10:50:05 -07:00
Winston Chang
205c35d5e5 Re-document with roxygen2 6.1.0 2018-08-03 17:50:47 -05:00
Winston Chang
bf0dd7d725 Merge pull request #1997 from rstudio/plot-cache
Add renderCachedPlot()
2018-08-03 13:30:13 -05:00
Winston Chang
ba2b811172 Fix argument name in documentation 2018-07-31 15:06:59 -05:00
Winston Chang
be347c3ed4 Don't cache plotResult 2018-07-31 12:07:21 -05:00
Joe Cheng
c01abdb6a9 Merge branch 'master' into plot-cache 2018-07-26 15:52:11 -07:00
Winston Chang
95a5a965a5 Documentation fixes 2018-07-20 16:12:21 -05:00
Winston Chang
fc2849a8ff Remove env and quoted arguments 2018-07-20 16:10:15 -05:00
Winston Chang
fcc900f3e0 Simplify resize logic 2018-07-20 14:48:57 -05:00
Winston Chang
9d0bcd5637 Add shiny-scalable class 2018-07-20 14:44:21 -05:00
Winston Chang
6ebbad5273 Safer file removal order 2018-07-19 23:20:51 -05:00
Winston Chang
930459899a Small logic cleanup 2018-07-19 23:20:40 -05:00
Winston Chang
fe730e2d76 Make session cache public, so that user can set it 2018-07-19 21:57:32 -05:00
Winston Chang
e58b2e9a47 Fix app- and session-level cache initialization 2018-07-19 21:57:07 -05:00
Winston Chang
719dbab0c2 DiskCache: make destroy_on_finalize default to FALSE 2018-07-19 14:35:59 -05:00
Winston Chang
86ea023e2e Update caches from code review feedback 2018-07-19 14:33:53 -05:00
Winston Chang
bc0fb3f44c Restructure drawReactive/renderFunc code 2018-07-18 14:52:39 -05:00
Winston Chang
6d37f6b4dd Set max-width and max-height to 100% for cached plots 2018-07-12 10:55:26 -05:00
Winston Chang
958ab85297 Add exec_missing parameter 2018-07-12 10:55:26 -05:00
Winston Chang
a23f973433 Suppress console logging for renderCachedPlot 2018-07-02 17:04:44 -05:00
Winston Chang
c124256bad Improved logging for DiskCache and MemoryCache 2018-07-02 17:04:28 -05:00
Winston Chang
f1b035bcca Update renderCachedPlot documentation and examples 2018-07-02 15:27:51 -05:00
Joe Cheng
81cc7c591e Merge pull request #2080 from schloerke/barret-freeze-thaw
freeze/thaw reactiveValues with namespace support
2018-06-29 15:58:52 -07:00
Winston Chang
a0ca560c3b Doc entries 2018-06-29 13:24:01 -05:00
Winston Chang
d1f20a9c73 Add 'missing' parameter to get() 2018-06-29 13:23:47 -05:00
Barret Schloerke
013059c5b9 merged in master 2018-06-29 11:35:20 -04:00
Winston Chang
fe6ad235ac Add sentinel value cache miss 2018-06-28 23:58:55 -05:00
Winston Chang
67af26ffe6 When MemoryCache is used, we can cache the displayList 2018-06-26 23:11:25 -05:00
Winston Chang
0fce9de04f Change default DiskCache size 2018-06-26 22:59:32 -05:00
Winston Chang
a8b8df21d6 DiskCache: make default finalizer behavior conditional on whether a temp directory is used 2018-06-26 22:57:35 -05:00
Winston Chang
ab2e304f02 DiskCache: Add info about sharing across processes 2018-06-26 22:56:59 -05:00
Winston Chang
574f2c53d4 DiskCache: Make destroy() work robustly with multiple processes 2018-06-26 22:26:27 -05:00
Winston Chang
bc85d812d2 DiskCache: Avoid errors from a race condition 2018-06-26 16:38:43 -05:00
Winston Chang
364990a29f Fix .rds directory search pattern 2018-06-26 16:37:58 -05:00
Winston Chang
9ac9e36873 DiskCache: Check if cache has been destroyed by someone else 2018-06-26 15:48:08 -05:00
Winston Chang
6745e09688 Add support for promisey cache key 2018-06-25 21:13:55 -05:00
Winston Chang
e758927c84 DiskCache: Add warning when caching reference objects 2018-06-25 15:53:47 -05:00
Winston Chang
90fbf7d50f Add comment about pruning 2018-06-25 15:34:43 -05:00
Winston Chang
75f1ee0082 Prune after setting value 2018-06-25 15:30:30 -05:00
Winston Chang
750aaf451a NEWS edits 2018-06-25 14:11:42 -05:00
Winston Chang
b44bfe9109 Grunt 2018-06-25 14:10:22 -05:00
Winston Chang
aa392f8563 Merge pull request #2102 from tmastny/selectize-remote
Resolves #1933: Serve-side selectize extended to all inputs
2018-06-25 14:07:56 -05:00
Winston Chang
ac7228f6c4 Merge branch 'master' into selectize-remote 2018-06-25 14:07:20 -05:00
Winston Chang
dcb12addaa Merge pull request #2108 from tmastny/par-oma
Resolves #1935: Fix coordinate outputs when modifying outer margins
2018-06-25 14:05:39 -05:00
Winston Chang
ad398b5f8a Merge branch 'master' into par-oma 2018-06-25 14:05:30 -05:00
Timothy Mastny
803cb4806e add new item 2018-06-25 14:04:31 -05:00
Timothy Mastny
1a468bbb61 add news items 2018-06-25 14:02:18 -05:00
Winston Chang
c332c051f3 Merge pull request #2099 from tmastny/slider-type
Fix #2019: `updateSliderInput` changes formatting
2018-06-25 14:00:18 -05:00
Timothy Mastny
db48befcb7 removed TODO comments 2018-06-25 13:51:24 -05:00
Winston Chang
b02edb05ac DiskCache: Use temp file when setting value 2018-06-25 11:56:01 -05:00
Winston Chang
d7009fd1c8 DiskCache: Don't call exists() before reading file 2018-06-25 11:55:29 -05:00
Winston Chang
ce3755676c Fix files that were split 2018-06-25 10:45:12 -05:00
Winston Chang
db3c1b728d Merge branches 'plot-cache-split-1' and 'plot-cache-split-2' into plot-cache 2018-06-25 10:42:52 -05:00
Winston Chang
1761de4740 Rename cache.R cache-context.R 2018-06-25 10:42:49 -05:00
Winston Chang
09d496925b Rename cache.R cache-memory.R 2018-06-25 10:41:49 -05:00
Winston Chang
3af5327f1c Rename cache.R cache-disk.R 2018-06-25 10:41:04 -05:00
Winston Chang
06cb14d7ec Rename $has method to $exists 2018-06-22 23:27:49 -05:00
Winston Chang
7be1a9d7fa Add memoryCache and make it the default 2018-06-22 23:08:39 -05:00
Winston Chang
95243fb35c DiskCache: Add LRU eviction policy 2018-06-22 22:44:27 -05:00
Winston Chang
26438a3979 DiskCache cleanup 2018-06-22 19:32:29 -05:00
Winston Chang
28db097a71 Use promise domain for currentOutputName 2018-06-22 19:31:20 -05:00
Winston Chang
76fdd8ae04 Make renderCachedPlot work with async 2018-06-22 16:47:55 -05:00
Timothy Mastny
3a73bfb142 changed output coordinate system to "ndc" to account for margin changes 2018-06-21 17:48:16 -05:00
Timothy Mastny
a24bdabf08 Updates to Winston's feedback: removed for ... of iteration that is not supported in IE. 2018-06-21 16:17:42 -05:00
Winston Chang
8815f293a2 Update R version check information 2018-06-21 15:55:35 -05:00
Winston Chang
9af2775539 Fix absolutePath to correctly handle absolute paths 2018-06-21 15:48:54 -05:00
Winston Chang
ae5deae6e9 Use output name in cache key 2018-06-21 14:55:30 -05:00
Winston Chang
61c2126498 Add diskCache function, and app- and session-level caches 2018-06-21 14:44:09 -05:00
Timothy Mastny
881fe0cfce explicitly set number to null instead of implicitly for better documentation 2018-06-21 14:30:15 -05:00
Timothy Mastny
a999bf389c update NEWS.md 2018-06-21 14:30:15 -05:00
Timothy Mastny
ff3b97b630 refactored data type checking for consistency 2018-06-21 14:30:15 -05:00
Timothy Mastny
639b520d39 updateInputSlider can now change from date to date-time formatting. fixes #2019 2018-06-21 14:30:15 -05:00
Timothy Mastny
19dc29ea17 changes as per Winston's feedback; additional comments, and edge cases, and removed unnecessary JS. 2018-06-21 12:47:14 -05:00
Timothy Mastny
97bebae8d7 fixed default selection for multiple-select 2018-06-20 15:41:35 -05:00
Timothy Mastny
cf534ce6da remove "thiz"s and replace with explicit "selectize" 2018-06-20 15:41:35 -05:00
Timothy Mastny
f25f691a55 fix selectize for default selected value NULL 2018-06-20 15:41:35 -05:00
Timothy Mastny
cbebf8be7b improve performance of R input processing 2018-06-20 15:41:35 -05:00
Timothy Mastny
165ce26b2f Fixes #1933. Fixed JSON encoding of input data, and added more optgroup controls on JS-side 2018-06-20 15:41:35 -05:00
Winston Chang
572c863bff Merge pull request #2106 from schloerke/js_patch
compile with grunt to get version in js code (v1.1.0.9000)
2018-06-20 12:47:43 -05:00
Barret Schloerke
d3c85d67b8 gruntfile should run 'default' task to make sure everything is built like normal 2018-06-20 13:31:10 -04:00
Barret Schloerke
ff3434f77e add a R test to make sure shiny.js {{ VERSION }} was replaced 2018-06-20 12:53:07 -04:00
Barret Schloerke
762528c044 add a grunt test to make sure {{ VERSION }} was replaced 2018-06-20 12:52:46 -04:00
Barret Schloerke
1891af0d4a compile with grunt 2018-06-20 11:39:12 -04:00
Winston Chang
583ad036f7 Streamline renderCachedPlot API 2018-06-19 16:00:49 -05:00
Winston Chang
ac92bf98d4 WIP 2018-06-18 16:25:36 -05:00
Winston Chang
fd90ff7ff7 Use DiskCache class 2018-06-18 16:25:36 -05:00
Winston Chang
d06dbbe5db Change cacheResetEventExpr to cacheResetExpr 2018-06-18 16:25:36 -05:00
Winston Chang
bffc4995d7 Rename normalizePath2 to absolutePath 2018-06-18 16:25:36 -05:00
Winston Chang
4b8b406bed Add sizeGrowthRatio function 2018-06-18 16:25:36 -05:00
Winston Chang
5641153272 renderCachedPlot: pass ... args 2018-06-18 16:25:36 -05:00
Winston Chang
08c6c7781f Rename cacheClearExpr to cacheResetEventExpr 2018-06-18 16:25:36 -05:00
Winston Chang
ad2ad391a7 Isolate user code 2018-06-18 16:25:36 -05:00
Winston Chang
caac88be0d Don't re-run user code when only dimensions change 2018-06-18 16:25:36 -05:00
Winston Chang
10660aa373 Rename cacheInvalidationExpr to cacheClearExpr 2018-06-18 16:25:36 -05:00
Winston Chang
cfaf97aee4 Add entries to staticdocs 2018-06-18 16:25:36 -05:00
Winston Chang
55f14576f0 Remove createCachedPlot function 2018-06-18 16:25:36 -05:00
Winston Chang
4dca94ac99 Code reorganization 2018-06-18 16:25:36 -05:00
Winston Chang
14779d3d27 Only invalidate plot when fitted dimensions change 2018-06-18 16:25:36 -05:00
Winston Chang
66d1e710b5 Allow renderCachedPlot to take a directory for scope 2018-06-18 16:25:36 -05:00
Winston Chang
12ae3c17e9 Allow onStop to take a NULL session 2018-06-18 16:25:35 -05:00
Winston Chang
36e4da0709 Add app/session scoping for renderCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
91631cb081 Note bug fix in R 2018-06-18 16:25:35 -05:00
Winston Chang
224f082e1f Implement cache invalidation 2018-06-18 16:25:35 -05:00
Winston Chang
76b239a6ea Convert renderCachedPlot to take expr and cacheKeyExpr 2018-06-18 16:25:35 -05:00
Winston Chang
cb476b510d Initial implementation of renderCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
334f233968 Move plot caching code into separate file 2018-06-18 16:25:35 -05:00
Winston Chang
e1f21250b9 Rename plotCache to createCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
8d087e4f20 Get output info and auto-size plot cache images 2018-06-18 16:25:35 -05:00
Winston Chang
9e35e8c947 Allow user code to pass width/height/pixelratio 2018-06-18 16:25:35 -05:00
Winston Chang
f98faef024 Document cache scoping and minor code cleanup 2018-06-18 16:25:35 -05:00
Winston Chang
0f9346ead5 Add different scoping levels for plotCache 2018-06-18 16:25:35 -05:00
Winston Chang
fc8118c694 First implementation of plotCache 2018-06-18 16:25:35 -05:00
Joe Cheng
026b7278c1 Merge pull request #2091 from schloerke/barret-varSelectInput
Variable Select Input
2018-06-14 09:39:09 -05:00
Barret Schloerke
375a7e7e5c respect existing class values and add tests 2018-06-11 10:31:49 -05:00
Barret Schloerke
7a1aecb1a4 varSelectInput doc tweaks 2018-06-08 10:59:45 -04:00
Barret Schloerke
b3690e8680 add bullets in details for output return value (update corresponding example) 2018-06-08 10:51:37 -04:00
Barret Schloerke
97d490cfb4 remove rogue staticdocs fn 2018-06-07 16:52:35 -04:00
Barret Schloerke
2081dda6fc merged master -> varSelectInput 2018-06-07 16:46:31 -04:00
Barret Schloerke
ea912fc50c staticdocs for varSelectInput 2018-06-07 16:43:20 -04:00
Barret Schloerke
b655fdf68f added news for varSelectInput 2018-06-07 16:43:05 -04:00
Barret Schloerke
4749f46a4f add shiny.symbol and shiny.symbolList tests 2018-06-07 16:10:47 -04:00
Barret Schloerke
f95bb9c82d compile and document 2018-06-07 16:10:24 -04:00
Barret Schloerke
6529529cdb add new input 'varSelectInput()' and input binding 'shiny.symbol' and 'shiny.symbolList' 2018-06-07 16:10:01 -04:00
Tim Mastny
3a2a3f21d4 Improve optgroup documentation per #1864 (#2084)
* improves optgroup documentation per #1864

* change PR number in NEWS.md

* change addresses to addressed
2018-06-07 12:09:45 -05:00
Tim Mastny
631bc1c481 Addresses #2042: lighten dates outside of range for datepicker (#2087)
* dehighlight dates outside of range for datepicker

* override color in shiny.css instead of package css

* refactored css styling to use specific references instead of !important
2018-06-07 11:57:27 -05:00
Barret Schloerke
597af36759 added news item 2018-05-31 10:31:35 -04:00
Barret Schloerke
691062f687 white space 2018-05-31 10:30:19 -04:00
Barret Schloerke
6651c4ea48 when freezing a reactivevalues key, use the name space similar to $.reactivevalues 2018-05-31 10:23:30 -04:00
Barret Schloerke
116559e5a0 use utils namespace 2018-05-31 10:21:55 -04:00
Barret Schloerke
7818e8ed64 white space 2018-05-31 10:21:42 -04:00
Winston Chang
2880391620 runApp: add support for IPv6 addresses 2018-05-25 16:19:51 -04:00
Joe Cheng
f742605a1b Bump version to *.9000. Back to work! 2018-05-17 17:20:17 -07:00
85 changed files with 4740 additions and 1061 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.1.0
Version: 1.1.0.9001
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -65,7 +65,7 @@ Depends:
Imports:
utils,
grDevices,
httpuv (>= 1.4.3),
httpuv (>= 1.4.3.9001),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
@@ -89,7 +89,10 @@ 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'
@@ -100,7 +103,10 @@ Collate:
'map.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'
@@ -139,6 +145,7 @@ Collate:
'priorityqueue.R'
'progress.R'
'react.R'
'render-cached-plot.R'
'render-plot.R'
'render-table.R'
'run-url.R'
@@ -155,4 +162,4 @@ Collate:
'test-export.R'
'timer.R'
'update-input.R'
RoxygenNote: 6.0.1.9000
RoxygenNote: 6.1.0

View File

@@ -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)
@@ -67,6 +68,7 @@ export(dateRangeInput)
export(dblclickOpts)
export(debounce)
export(dialogViewer)
export(diskCache)
export(div)
export(downloadButton)
export(downloadHandler)
@@ -90,6 +92,7 @@ export(fluidRow)
export(formatStackTrace)
export(freezeReactiveVal)
export(freezeReactiveValue)
export(getCurrentOutputInfo)
export(getDefaultReactiveDomain)
export(getQueryString)
export(getShinyOption)
@@ -121,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)
@@ -128,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)
@@ -138,6 +143,7 @@ export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
export(maskReactiveContext)
export(memoryCache)
export(modalButton)
export(modalDialog)
export(navbarMenu)
@@ -189,6 +195,7 @@ export(removeModal)
export(removeNotification)
export(removeTab)
export(removeUI)
export(renderCachedPlot)
export(renderDataTable)
export(renderImage)
export(renderPlot)
@@ -226,6 +233,7 @@ export(showTab)
export(sidebarLayout)
export(sidebarPanel)
export(singleton)
export(sizeGrowthRatio)
export(sliderInput)
export(snapshotExclude)
export(snapshotPreprocessInput)
@@ -268,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)

42
NEWS.md
View File

@@ -1,3 +1,35 @@
shiny 1.1.0.9001
===========
## Full changelog
### Minor new features and improvements
* Added `renderCachedPlot()`, which stores plots in a cache so that they can be served up almost instantly. ([#1997](https://github.com/rstudio/shiny/pull/1997))
* Support for selecting variables of a data frame with the output values to be used within tidy evaluation. Added functions: `varSelectInput`, `varSelectizeInput`, `updateVarSelectInput`, `updateVarSelectizeInput`. ([#2091](https://github.com/rstudio/shiny/pull/2091))
* Addressed [#2042](https://github.com/rstudio/shiny/issues/2042): dates outside of `min`/`max` date range are now a lighter shade of grey to highlight the allowed range. ([#2087](https://github.com/rstudio/shiny/pull/2087))
* Added support for plot interaction when the plot is scaled. ([#2125](https://github.com/rstudio/shiny/pull/2125))
* Fixed [#1933](https://github.com/rstudio/shiny/issues/1933): extended server-side selectize to lists and optgroups. ([#2102](https://github.com/rstudio/shiny/pull/2102))
* Fixed [#1935](https://github.com/rstudio/shiny/issues/1935): correctly returns plot coordinates when using outer margins. ([#2108](https://github.com/rstudio/shiny/pull/2108))
* 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)
* Fixed [#2138](https://github.com/rstudio/shiny/issues/2138): Inputs that are part of a `renderUI` were no longer restoring correctly from bookmarked state. [#2139](https://github.com/rstudio/shiny/pull/2139)
* Fixed [#2093](https://github.com/rstudio/shiny/issues/2093): Make sure bookmark scope directory does not exist before trying to create it. [#2168](https://github.com/rstudio/shiny/pull/2168)
### Documentation Updates
* Addressed [#1864](https://github.com/rstudio/shiny/issues/1864) by changing `optgroup` documentation to use `list` instead of `c`. ([#2084](https://github.com/rstudio/shiny/pull/2084))
shiny 1.1.0
===========
@@ -282,7 +314,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))
@@ -681,7 +713,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
@@ -772,13 +804,13 @@ Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version,
### Known issues for migration
* In Bootstrap 3, images in `<img>` tags are no longer automatically scaled to the width of their container. If you use `img()` in your UI code, or `<img>` tags in your raw HTML source, it's possible that they will be too large in the new version of Shiny. To address this you can add the `img-responsive` class:
```r
img(src = "picture.png", class = "img-responsive")
```
The R code above will generate the following HTML:
```html
<img src="picture.png" class="img-responsive">
```

View File

@@ -448,7 +448,13 @@ withRestoreContext <- function(ctx, expr) {
# Is there a current restore context?
hasCurrentRestoreContext <- function() {
restoreCtxStack$size() > 0
if (restoreCtxStack$size() > 0)
return(TRUE)
domain <- getDefaultReactiveDomain()
if (!is.null(domain) && !is.null(domain$restoreContext))
return(TRUE)
return(FALSE)
}
# Call to access the current restore context. First look on the restore

561
R/cache-disk.R Normal file
View 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
View 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
View 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.")
}
}

View File

@@ -249,14 +249,20 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
x <- asNumber(df[[xvar]])
y <- asNumber(df[[yvar]])
# Get the pixel coordinates of the point
coordPx <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
# Get the coordinates of the point (in img pixel coordinates)
point_img <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
# Get pixel coordinates of data points
dataPx <- scaleCoords(x, y, coordinfo)
# Get coordinates of data points (in img pixel coordinates)
data_img <- scaleCoords(x, y, coordinfo)
# Distances of data points to coordPx
dists <- sqrt((dataPx$x - coordPx$x) ^ 2 + (dataPx$y - coordPx$y) ^ 2)
# Get x/y distances (in css coordinates)
dist_css <- list(
x = (data_img$x - point_img$x) / coordinfo$pixelratio$x,
y = (data_img$y - point_img$y) / coordinfo$pixelratio$y
)
# Distances of data points to the target point, in css pixels.
dists <- sqrt(dist_css$x^2 + dist_css$y^2)
if (addDist)
df$dist_ <- dists
@@ -298,50 +304,56 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# The coordinfo data structure will look something like the examples below.
# For base graphics, `mapping` is empty, and there are no panelvars:
# List of 7
# $ x : num 4.37
# $ y : num 12
# $ mapping: Named list()
# $ domain :List of 4
# $ x : num 4.37
# $ y : num 12
# $ pixelratio:List of 2
# ..$ x: num 2
# ..$ y: num 2
# $ mapping : Named list()
# $ domain :List of 4
# ..$ left : num 1.36
# ..$ right : num 5.58
# ..$ bottom: num 9.46
# ..$ top : num 34.8
# $ range :List of 4
# $ range :List of 4
# ..$ left : num 58
# ..$ right : num 429
# ..$ bottom: num 226
# ..$ top : num 58
# $ log :List of 2
# $ log :List of 2
# ..$ x: NULL
# ..$ y: NULL
# $ .nonce : num 0.343
# $ .nonce : num 0.343
#
# For ggplot2, the mapping vars usually will be included, and if faceting is
# used, they will be listed as panelvars:
# List of 9
# $ x : num 3.78
# $ y : num 17.1
# $ panelvar1: int 6
# $ panelvar2: int 0
# $ mapping :List of 4
# $ x : num 3.78
# $ y : num 17.1
# $ pixelratio:List of 2
# ..$ x: num 2
# ..$ y: num 2
# $ panelvar1 : int 6
# $ panelvar2 : int 0
# $ mapping :List of 4
# ..$ x : chr "wt"
# ..$ y : chr "mpg"
# ..$ panelvar1: chr "cyl"
# ..$ panelvar2: chr "am"
# $ domain :List of 4
# $ domain :List of 4
# ..$ left : num 1.32
# ..$ right : num 5.62
# ..$ bottom: num 9.22
# ..$ top : num 35.1
# $ range :List of 4
# $ range :List of 4
# ..$ left : num 172
# ..$ right : num 300
# ..$ bottom: num 144
# ..$ top : num 28.5
# $ log :List of 2
# $ log :List of 2
# ..$ x: NULL
# ..$ y: NULL
# $ .nonce : num 0.603
# $ .nonce : num 0.603

View File

@@ -33,7 +33,7 @@
#' @return A select list control that can be added to a UI definition.
#'
#' @family input elements
#' @seealso \code{\link{updateSelectInput}}
#' @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
)
}

View File

@@ -86,24 +86,10 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
version = "0.10.2.2")
}
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"
dataType <- getSliderType(min, max, 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.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
}
# Restore bookmarked values here, after doing the type checking, because the
@@ -250,7 +236,7 @@ findStepSize <- function(min, max, step) {
# values to calculate the step size.
pretty_steps <- pretty(c(min, max), n = 100)
n_steps <- length(pretty_steps) - 1
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
# which should be way way less significant than any data we'd want to keep.

588
R/render-cached-plot.R Normal file
View File

@@ -0,0 +1,588 @@
#' Plot output with cached images
#'
#' Renders a reactive plot, with plot images cached to disk.
#'
#' \code{expr} is an expression that generates a plot, similar to that in
#' \code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
#' take reactive dependencies. It is re-executed only when the cache key
#' changes.
#'
#' \code{cacheKeyExpr} is an expression which, when evaluated, returns an object
#' which will be serialized and hashed using the \code{\link[digest]{digest}}
#' function to generate a string that will be used as a cache key. This key is
#' used to identify the contents of the plot: if the cache key is the same as a
#' previous time, it assumes that the plot is the same and can be retrieved from
#' the cache.
#'
#' This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
#' upstream reactives are invalidated. This will also trigger re-execution of
#' the plotting expression, \code{expr}.
#'
#' The key should consist of "normal" R objects, like vectors and lists. Lists
#' should in turn contain other normal R objects. If the key contains
#' environments, external pointers, or reference objects -- or even if it has
#' such objects attached as attributes -- then it is possible that it will
#' change unpredictably even when you do not expect it to. Additionally, because
#' the entire key is serialized and hashed, if it contains a very large object
#' -- a large data set, for example -- there may be a noticeable performance
#' penalty.
#'
#' If you face these issues with the cache key, you can work around them by
#' extracting out the important parts of the objects, and/or by converting them
#' to normal R objects before returning them. Your expression could even
#' serialize and hash that information in an efficient way and return a string,
#' which will in turn be hashed (very quickly) by the
#' \code{\link[digest]{digest}} function.
#'
#' Internally, the result from \code{cacheKeyExpr} is combined with the name of
#' the output (if you assign it to \code{output$plot1}, it will be combined
#' with \code{"plot1"}) to form the actual key that is used. As a result, even
#' if there are multiple plots that have the same \code{cacheKeyExpr}, they
#' will not have cache key collisions.
#'
#' @section Cache scoping:
#'
#' There are a number of different ways you may want to scope the cache. For
#' example, you may want each user session to have their own plot cache, or
#' you may want each run of the application to have a cache (shared among
#' possibly multiple simultaneous user sessions), or you may want to have a
#' cache that persists even after the application is shut down and started
#' again.
#'
#' To control the scope of the cache, use the \code{cache} parameter. There
#' are two ways of having Shiny automatically create and clean up the disk
#' cache.
#'
#' \describe{
#' \item{1}{To scope the cache to one run of a Shiny application (shared
#' among possibly multiple user sessions), use \code{cache="app"}. This
#' is the default. The cache will be shared across multiple sessions, so
#' there is potentially a large performance benefit if there are many users
#' of the application. When the application stops running, the cache will
#' be deleted. If plots cannot be safely shared across users, this should
#' not be used.}
#' \item{2}{To scope the cache to one session, use \code{cache="session"}.
#' When a new user session starts -- in other words, when a web browser
#' visits the Shiny application -- a new cache will be created on disk
#' for that session. When the session ends, the cache will be deleted.
#' The cache will not be shared across multiple sessions.}
#' }
#'
#' If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
#' in size, and will be stored stored in memory, using a
#' \code{\link{memoryCache}} object. Note that the cache space will be shared
#' among all cached plots within a single application or session.
#'
#' In some cases, you may want more control over the caching behavior. For
#' example, you may want to use a larger or smaller cache, share a cache
#' among multiple R processes, or you may want the cache to persist across
#' multiple runs of an application, or even across multiple R processes.
#'
#' To use different settings for an application-scoped cache, you can call
#' \code{\link{shinyOptions}()} at the top of your app.R, server.R, or
#' global.R. For example, this will create a cache with 20 MB of space
#' instead of the default 10 MB:
#' \preformatted{
#' shinyOptions(cache = memoryCache(size = 20e6))
#' }
#'
#' To use different settings for a session-scoped cache, you can call
#' \code{\link{shinyOptions}()} at the top of your server function. To use
#' the session-scoped cache, you must also call \code{renderCachedPlot} with
#' \code{cache="session"}. This will create a 20 MB cache for the session:
#' \preformatted{
#' function(input, output, session) {
#' shinyOptions(cache = memoryCache(size = 20e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
#' cache = "session"
#' )
#' }
#' }
#'
#' If you want to create a cache that is shared across multiple concurrent
#' R processes, you can use a \code{\link{diskCache}}. You can create an
#' application-level shared cache by putting this at the top of your app.R,
#' server.R, or global.R:
#' \preformatted{
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#' }
#'
#' This will create a subdirectory in your system temp directory named
#' \code{myapp-cache} (replace \code{myapp-cache} with a unique name of
#' your choosing). On most platforms, this directory will be removed when
#' your system reboots. This cache will persist across multiple starts and
#' stops of the R process, as long as you do not reboot.
#'
#' To have the cache persist even across multiple reboots, you can create the
#' cache in a location outside of the temp directory. For example, it could
#' be a subdirectory of the application:
#' \preformatted{
#' shinyOptions(cache = diskCache("./myapp-cache"))
#' }
#'
#' In this case, resetting the cache will have to be done manually, by deleting
#' the directory.
#'
#' You can also scope a cache to just one plot, or selected plots. To do that,
#' create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
#' as the \code{cache} argument of \code{renderCachedPlot}.
#'
#' @section Interactive plots:
#'
#' \code{renderCachedPlot} can be used to create interactive plots. See
#' \code{\link{plotOutput}} for more information and examples.
#'
#'
#' @inheritParams renderPlot
#' @param cacheKeyExpr An expression that returns a cache key. This key should
#' be a unique identifier for a plot: the assumption is that if the cache key
#' is the same, then the plot will be the same.
#' @param sizePolicy A function that takes two arguments, \code{width} and
#' \code{height}, and returns a list with \code{width} and \code{height}. The
#' purpose is to round the actual pixel dimensions from the browser to some
#' other dimensions, so that this will not generate and cache images of every
#' possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
#' information on the default sizing policy.
#' @param res The resolution of the PNG, in pixels per inch.
#' @param cache The scope of the cache, or a cache object. This can be
#' \code{"app"} (the default), \code{"session"}, or a cache object like
#' a \code{\link{diskCache}}. See the Cache Scoping section for more
#' information.
#'
#' @seealso See \code{\link{renderPlot}} for the regular, non-cached version of
#' this function. For more about configuring caches, see
#' \code{\link{memoryCache}} and \code{\link{diskCache}}.
#'
#'
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' # A basic example that uses the default app-scoped memory cache.
#' # The cache will be shared among all simultaneous users of the application.
#' shinyApp(
#' fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
#' ),
#' mainPanel(plotOutput("plot"))
#' )
#' ),
#' function(input, output, session) {
#' output$plot <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) }
#' )
#' }
#' )
#'
#'
#'
#' # An example uses a data object shared across sessions. mydata() is part of
#' # the cache key, so when its value changes, plots that were previously
#' # stored in the cache will no longer be used (unless mydata() changes back
#' # to its previous value).
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
#'
#' ui <- fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
#' actionButton("newdata", "New data")
#' ),
#' mainPanel(
#' plotOutput("plot")
#' )
#' )
#' )
#'
#' server <- function(input, output, session) {
#' observeEvent(input$newdata, {
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
#' })
#'
#' output$plot <- renderCachedPlot(
#' {
#' Sys.sleep(2)
#' d <- mydata()
#' seqn <- seq_len(input$n)
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
#' },
#' cacheKeyExpr = { list(input$n, mydata()) },
#' )
#' }
#'
#' shinyApp(ui, server)
#'
#'
#' # A basic application with two plots, where each plot in each session has
#' # a separate cache.
#' shinyApp(
#' fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
#' ),
#' mainPanel(
#' plotOutput("plot1"),
#' plotOutput("plot2")
#' )
#' )
#' ),
#' function(input, output, session) {
#' output$plot1 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' )
#' output$plot2 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' )
#' }
#' )
#'
#' }
#'
#' \dontrun{
#' # At the top of app.R, this set the application-scoped cache to be a memory
#' # cache that is 20 MB in size, and where cached objects expire after one
#' # hour.
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and is
#' # deleted when the system reboots.
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and
#' # persists on disk across reboots.
#' shinyOptions(cache = diskCache("./myapp-cache"))
#'
#' # At the top of the server function, this set the session-scoped cache to be
#' # a memory cache that is 5 MB in size.
#' server <- function(input, output, session) {
#' shinyOptions(cache = memoryCache(max_size = 5e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
#' cache = "session"
#' )
#' }
#'
#' }
#' @export
renderCachedPlot <- function(expr,
cacheKeyExpr,
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
res = 72,
cache = "app",
...,
outputArgs = list()
) {
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
# This is so that the expr doesn't re-execute by itself; it needs to be
# triggered by the cache key (or width/height) changing.
isolatedFunc <- function() isolate(func())
args <- list(...)
cacheKeyExpr <- substitute(cacheKeyExpr)
# The real cache key we'll use also includes width, height, res, pixelratio.
# This is just the part supplied by the user.
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
ensureCacheSetup <- function() {
# For our purposes, cache objects must support these methods.
isCacheObject <- function(x) {
# Use tryCatch in case the object does not support `$`.
tryCatch(
is.function(x$get) && is.function(x$set),
error = function(e) FALSE
)
}
if (isCacheObject(cache)) {
# If `cache` is already a cache object, do nothing
return()
} else if (identical(cache, "app")) {
cache <<- getShinyOption("cache")
} else if (identical(cache, "session")) {
cache <<- session$cache
} else {
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
}
}
# The width and height of the plot to draw, given from sizePolicy. These
# values get filled by an observer below.
fitDims <- reactiveValues(width = NULL, height = NULL)
resizeObserver <- NULL
ensureResizeObserver <- function() {
if (!is.null(resizeObserver))
return()
# Given the actual width/height of the image in the browser, this gets the
# width/height from sizePolicy() and pushes those values into `fitDims`.
# It's done this way so that the `fitDims` only change (and cause
# invalidations) when the rendered image size changes, and not every time
# the browser's <img> tag changes size.
doResizeCheck <- function() {
width <- session$clientData[[paste0('output_', outputName, '_width')]]
height <- session$clientData[[paste0('output_', outputName, '_height')]]
if (is.null(width)) width <- 0
if (is.null(height)) height <- 0
rect <- sizePolicy(c(width, height))
fitDims$width <- rect[1]
fitDims$height <- rect[2]
}
# Run it once immediately, then set up the observer
isolate(doResizeCheck())
resizeObserver <<- observe(doResizeCheck())
}
# Vars to store session and output, so that they can be accessed from
# the plotObj() reactive.
session <- NULL
outputName <- NULL
drawReactive <- reactive(label = "plotObj", {
hybrid_chain(
# Depend on the user cache key, even though we don't use the value. When
# it changes, it can cause the drawReactive to re-execute. (Though
# drawReactive will not necessarily re-execute -- it must be called from
# renderFunc, which happens only if there's a cache miss.)
userCacheKey(),
function(userCacheKeyValue) {
# Get width/height, but don't depend on them.
isolate({
width <- fitDims$width
height <- fitDims$height
})
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("drawPlot", c(
list(
name = outputName,
session = session,
func = isolatedFunc,
width = width,
height = height,
pixelratio = pixelratio,
res = res
),
args
))
},
catch = function(reason) {
# Non-isolating read. A common reason for errors in plotting is because
# the dimensions are too small. By taking a dependency on width/height,
# we can try again if the plot output element changes size.
fitDims$width
fitDims$height
# Propagate the error
stop(reason)
}
)
})
# This function is the one that's returned from renderPlot(), and gets
# wrapped in an observer when the output value is assigned.
renderFunc <- function(shinysession, name, ...) {
outputName <<- name
session <<- shinysession
ensureCacheSetup()
ensureResizeObserver()
hybrid_chain(
# This use of the userCacheKey() sets up the reactive dependency that
# causes plot re-draw events. These may involve pulling from the cache,
# replaying a display list, or re-executing user code.
userCacheKey(),
function(userCacheKeyResult) {
width <- fitDims$width
height <- fitDims$height
pixelratio <- session$clientData$pixelratio %OR% 1
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
plotObj <- cache$get(key)
# First look in cache.
# Case 1. cache hit.
if (!is.key_missing(plotObj)) {
return(list(
cacheHit = TRUE,
key = key,
plotObj = plotObj,
width = width,
height = height,
pixelratio = pixelratio
))
}
# If not in cache, hybrid_chain call to drawReactive
#
# Two more possible cases:
# 2. drawReactive will re-execute and return a plot that's the
# correct size.
# 3. It will not re-execute, but it will return the previous value,
# which is the wrong size. It will include a valid display list
# which can be used by resizeSavedPlot.
hybrid_chain(
drawReactive(),
function(drawReactiveResult) {
# Pass along the key for caching in the next stage
list(
cacheHit = FALSE,
key = key,
plotObj = drawReactiveResult,
width = width,
height = height,
pixelratio = pixelratio
)
}
)
},
function(result) {
width <- result$width
height <- result$height
pixelratio <- result$pixelratio
# Three possibilities when we get here:
# 1. There was a cache hit. No need to set a value in the cache.
# 2. There was a cache miss, and the plotObj is already the correct
# size (because drawReactive re-executed). In this case, we need
# to cache it.
# 3. There was a cache miss, and the plotObj was not the corect size.
# In this case, we need to replay the display list, and then cache
# the result.
if (!result$cacheHit) {
# If the image is already the correct size, this just returns the
# object unchanged.
result$plotObj <- do.call("resizeSavedPlot", c(
list(
name,
shinysession,
result$plotObj,
width,
height,
pixelratio,
res
),
args
))
# Save a cached copy of the plotObj. The recorded displaylist for
# the plot can't be serialized and restored properly within the same
# R session, so we NULL it out before saving. (The image data and
# other metadata be saved and restored just fine.) Displaylists can
# also be very large (~1.5MB for a basic ggplot), and they would not
# be commonly used. Note that displaylist serialization was fixed in
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
# doesn't need to serialize objects, so it could actually save a
# display list, but for the reasons listed previously, it's
# generally not worth it.
# The plotResult is not the same as the recordedPlot (it is used to
# retrieve coordmap information for ggplot2 objects) but it is only
# used in conjunction with the recordedPlot, and we'll remove it
# because it can be quite large.
result$plotObj$plotResult <- NULL
result$plotObj$recordedPlot <- NULL
cache$set(result$key, result$plotObj)
}
img <- result$plotObj$img
# Replace exact pixel dimensions; instead, the max-height and
# max-width will be set to 100% from CSS.
img$class <- "shiny-scalable"
img$width <- NULL
img$height <- NULL
img
}
)
}
# If renderPlot isn't going to adapt to the height of the div, then the
# div needs to adapt to the height of renderPlot. By default, plotOutput
# sets the height to 400px, so to make it adapt we need to override it
# with NULL.
outputFunc <- plotOutput
formals(outputFunc)['height'] <- list(NULL)
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
}
#' Create a sizing function that grows at a given ratio
#'
#' Returns a function which takes a two-element vector representing an input
#' width and height, and returns a two-element vector of width and height. The
#' possible widths are the base width times the growthRate to any integer power.
#' For example, with a base width of 500 and growth rate of 1.25, the possible
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
#' widths.
#'
#' @param width,height Base width and height.
#' @param growthRate Growth rate multiplier.
#'
#' @seealso This is to be used with \code{\link{renderCachedPlot}}.
#'
#' @examples
#' f <- sizeGrowthRatio(500, 500, 1.25)
#' f(c(400, 400))
#' f(c(500, 500))
#' f(c(530, 550))
#' f(c(625, 700))
#'
#' @export
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
round_dim_up <- function(x, base, rate) {
power <- ceiling(log(x / base, rate))
ceiling(base * rate^power)
}
function(dims) {
if (length(dims) != 2) {
stop("dims must be a vector with two numbers, for width and height.")
}
c(
round_dim_up(dims[1], width, growthRate),
round_dim_up(dims[2], height, growthRate)
)
}
}

View File

@@ -133,10 +133,12 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
function(result) {
dims <- getDims()
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("resizeSavedPlot", c(
result <- do.call("resizeSavedPlot", c(
list(name, shinysession, result, dims$width, dims$height, pixelratio, res),
args
))
result$img
}
)
}
@@ -154,23 +156,25 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
if (result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res) {
return(result$img)
return(result)
}
coordmap <- NULL
outfile <- plotPNG(function() {
grDevices::replayPlot(result$recordedPlot)
coordmap <<- getCoordmap(result$plotResult, width, height, pixelratio, res)
coordmap <<- getCoordmap(result$plotResult, width*pixelratio, height*pixelratio, res*pixelratio)
}, width = width*pixelratio, height = height*pixelratio, res = res*pixelratio, ...)
on.exit(unlink(outfile), add = TRUE)
img <- list(
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, ...) {
@@ -227,7 +231,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
list(
plotResult = value,
recordedPlot = grDevices::recordPlot(),
coordmap = getCoordmap(value, width, height, pixelratio, res),
coordmap = getCoordmap(value, width*pixelratio, height*pixelratio, res*pixelratio),
pixelratio = pixelratio,
res = res
)
@@ -247,6 +251,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
# Get coordmap error message if present
error = attr(result$coordmap, "error", exact = TRUE)
))
result
},
finally = function() {
@@ -279,22 +284,26 @@ custom_print.ggplot <- function(x) {
# below. For base graphics:
# plot(mtcars$wt, mtcars$mpg)
# str(getPrevPlotCoordmap(400, 300))
# List of 1
# $ :List of 4
# ..$ domain :List of 4
# .. ..$ left : num 1.36
# .. ..$ right : num 5.58
# .. ..$ bottom: num 9.46
# .. ..$ top : num 34.8
# ..$ range :List of 4
# .. ..$ left : num 50.4
# .. ..$ right : num 373
# .. ..$ bottom: num 199
# .. ..$ top : num 79.6
# ..$ log :List of 2
# .. ..$ x: NULL
# .. ..$ y: NULL
# ..$ mapping: Named list()
# List of 2
# $ panels:List of 1
# ..$ :List of 4
# .. ..$ domain :List of 4
# .. .. ..$ left : num 1.36
# .. .. ..$ right : num 5.58
# .. .. ..$ bottom: num 9.46
# .. .. ..$ top : num 34.8
# .. ..$ range :List of 4
# .. .. ..$ left : num 65.6
# .. .. ..$ right : num 366
# .. .. ..$ bottom: num 238
# .. .. ..$ top : num 48.2
# .. ..$ log :List of 2
# .. .. ..$ x: NULL
# .. .. ..$ y: NULL
# .. ..$ mapping: Named list()
# $ dims :List of 2
# ..$ width : num 400
# ..$ height: num 300
#
# For ggplot2, first you need to define the print.ggplot function from inside
# renderPlot, then use it to print the plot:
@@ -313,29 +322,33 @@ custom_print.ggplot <- function(x) {
# }
#
# p <- print(ggplot(mtcars, aes(wt, mpg)) + geom_point())
# str(getGgplotCoordmap(p, 1, 72))
# List of 1
# $ :List of 10
# ..$ panel : int 1
# ..$ row : int 1
# ..$ col : int 1
# ..$ panel_vars: Named list()
# ..$ log :List of 2
# .. ..$ x: NULL
# .. ..$ y: NULL
# ..$ domain :List of 4
# .. ..$ left : num 1.32
# .. ..$ right : num 5.62
# .. ..$ bottom: num 9.22
# .. ..$ top : num 35.1
# ..$ mapping :List of 2
# .. ..$ x: chr "wt"
# .. ..$ y: chr "mpg"
# ..$ range :List of 4
# .. ..$ left : num 40.8
# .. ..$ right : num 446
# .. ..$ bottom: num 263
# .. ..$ top : num 14.4
# str(getGgplotCoordmap(p, 400, 300, 72))
# List of 2
# $ panels:List of 1
# ..$ :List of 8
# .. ..$ panel : num 1
# .. ..$ row : num 1
# .. ..$ col : num 1
# .. ..$ panel_vars: Named list()
# .. ..$ log :List of 2
# .. .. ..$ x: NULL
# .. .. ..$ y: NULL
# .. ..$ domain :List of 4
# .. .. ..$ left : num 1.32
# .. .. ..$ right : num 5.62
# .. .. ..$ bottom: num 9.22
# .. .. ..$ top : num 35.1
# .. ..$ mapping :List of 2
# .. .. ..$ x: chr "wt"
# .. .. ..$ y: chr "mpg"
# .. ..$ range :List of 4
# .. .. ..$ left : num 33.3
# .. .. ..$ right : num 355
# .. .. ..$ bottom: num 328
# .. .. ..$ top : num 5.48
# $ dims :List of 2
# ..$ width : num 400
# ..$ height: num 300
#
# With a faceted ggplot2 plot, the outer list contains two objects, each of
# which represents one panel. In this example, there is one panelvar, but there
@@ -343,59 +356,63 @@ custom_print.ggplot <- function(x) {
# mtc <- mtcars
# mtc$am <- factor(mtc$am)
# p <- print(ggplot(mtc, aes(wt, mpg)) + geom_point() + facet_wrap(~ am))
# str(getGgplotCoordmap(p, 1, 72))
# str(getGgplotCoordmap(p, 400, 300, 72))
# List of 2
# $ :List of 10
# ..$ panel : int 1
# ..$ row : int 1
# ..$ col : int 1
# ..$ panel_vars:List of 1
# .. ..$ panelvar1: Factor w/ 2 levels "0","1": 1
# ..$ log :List of 2
# .. ..$ x: NULL
# .. ..$ y: NULL
# ..$ domain :List of 4
# .. ..$ left : num 1.32
# .. ..$ right : num 5.62
# .. ..$ bottom: num 9.22
# .. ..$ top : num 35.1
# ..$ mapping :List of 3
# .. ..$ x : chr "wt"
# .. ..$ y : chr "mpg"
# .. ..$ panelvar1: chr "am"
# ..$ range :List of 4
# .. ..$ left : num 45.6
# .. ..$ right : num 317
# .. ..$ bottom: num 251
# .. ..$ top : num 35.7
# $ :List of 10
# ..$ panel : int 2
# ..$ row : int 1
# ..$ col : int 2
# ..$ panel_vars:List of 1
# .. ..$ panelvar1: Factor w/ 2 levels "0","1": 2
# ..$ log :List of 2
# .. ..$ x: NULL
# .. ..$ y: NULL
# ..$ domain :List of 4
# .. ..$ left : num 1.32
# .. ..$ right : num 5.62
# .. ..$ bottom: num 9.22
# .. ..$ top : num 35.1
# ..$ mapping :List of 3
# .. ..$ x : chr "wt"
# .. ..$ y : chr "mpg"
# .. ..$ panelvar1: chr "am"
# ..$ range :List of 4
# .. ..$ left : num 322
# .. ..$ right : num 594
# .. ..$ bottom: num 251
# .. ..$ top : num 35.7
# $ panels:List of 2
# ..$ :List of 8
# .. ..$ panel : num 1
# .. ..$ row : int 1
# .. ..$ col : int 1
# .. ..$ panel_vars:List of 1
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 1
# .. ..$ log :List of 2
# .. .. ..$ x: NULL
# .. .. ..$ y: NULL
# .. ..$ domain :List of 4
# .. .. ..$ left : num 1.32
# .. .. ..$ right : num 5.62
# .. .. ..$ bottom: num 9.22
# .. .. ..$ top : num 35.1
# .. ..$ mapping :List of 3
# .. .. ..$ x : chr "wt"
# .. .. ..$ y : chr "mpg"
# .. .. ..$ panelvar1: chr "am"
# .. ..$ range :List of 4
# .. .. ..$ left : num 33.3
# .. .. ..$ right : num 191
# .. .. ..$ bottom: num 328
# .. .. ..$ top : num 23.1
# ..$ :List of 8
# .. ..$ panel : num 2
# .. ..$ row : int 1
# .. ..$ col : int 2
# .. ..$ panel_vars:List of 1
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 2
# .. ..$ log :List of 2
# .. .. ..$ x: NULL
# .. .. ..$ y: NULL
# .. ..$ domain :List of 4
# .. .. ..$ left : num 1.32
# .. .. ..$ right : num 5.62
# .. .. ..$ bottom: num 9.22
# .. .. ..$ top : num 35.1
# .. ..$ mapping :List of 3
# .. .. ..$ x : chr "wt"
# .. .. ..$ y : chr "mpg"
# .. .. ..$ panelvar1: chr "am"
# .. ..$ range :List of 4
# .. .. ..$ left : num 197
# .. .. ..$ right : num 355
# .. .. ..$ bottom: num 328
# .. .. ..$ top : num 23.1
# $ dims :List of 2
# ..$ width : num 400
# ..$ height: num 300
getCoordmap <- function(x, width, height, pixelratio, res) {
getCoordmap <- function(x, width, height, res) {
if (inherits(x, "ggplot_build_gtable")) {
getGgplotCoordmap(x, pixelratio, res)
getGgplotCoordmap(x, width, height, res)
} else {
getPrevPlotCoordmap(width, height)
}
@@ -415,7 +432,7 @@ getPrevPlotCoordmap <- function(width, height) {
}
# Wrapped in double list because other types of plots can have multiple panels.
list(list(
panel_info <- list(list(
# Bounds of the plot area, in data space
domain = list(
left = usrCoords[1],
@@ -425,10 +442,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,
@@ -439,27 +456,43 @@ getPrevPlotCoordmap <- function(width, height) {
# (not an array) in JSON.
mapping = list(x = NULL)[0]
))
list(
panels = panel_info,
dims = list(
width = width,
height =height
)
)
}
# Given a ggplot_build_gtable object, return a coordmap for it.
getGgplotCoordmap <- function(p, pixelratio, res) {
getGgplotCoordmap <- function(p, width, height, res) {
if (!inherits(p, "ggplot_build_gtable"))
return(NULL)
tryCatch({
# Get info from built ggplot object
info <- find_panel_info(p$build)
panel_info <- find_panel_info(p$build)
# Get ranges from gtable - it's possible for this to return more elements than
# info, because it calculates positions even for panels that aren't present.
# This can happen with facet_wrap.
ranges <- find_panel_ranges(p$gtable, pixelratio, res)
ranges <- find_panel_ranges(p$gtable, res)
for (i in seq_along(info)) {
info[[i]]$range <- ranges[[i]]
for (i in seq_along(panel_info)) {
panel_info[[i]]$range <- ranges[[i]]
}
return(info)
return(
list(
panels = panel_info,
dims = list(
width = width,
height = height
)
)
)
}, error = function(e) {
# If there was an error extracting info from the ggplot object, just return
@@ -486,13 +519,11 @@ find_panel_info <- function(b) {
# This is for ggplot2>2.2.1, after an API was introduced for extracting
# information about the plot object.
find_panel_info_api <- function(b) {
# Workaround for check NOTE, until ggplot2 >2.2.1 is released
colon_colon <- `::`
# Given a built ggplot object, return x and y domains (data space coords) for
# each panel.
layout <- colon_colon("ggplot2", "summarise_layout")(b)
coord <- colon_colon("ggplot2", "summarise_coord")(b)
layers <- colon_colon("ggplot2", "summarise_layers")(b)
layout <- ggplot2::summarise_layout(b)
coord <- ggplot2::summarise_coord(b)
layers <- ggplot2::summarise_layers(b)
# Given x and y scale objects and a coord object, return a list that has
# the bases of log transformations for x and y, or NULL if it's not a
@@ -822,7 +853,7 @@ find_panel_info_non_api <- function(b, ggplot_format) {
# Given a gtable object, return the x and y ranges (in pixel dimensions)
find_panel_ranges <- function(g, pixelratio, res) {
find_panel_ranges <- function(g, res) {
# Given a vector of unit objects, return logical vector indicating which ones
# are "null" units. These units use the remaining available width/height --
# that is, the space not occupied by elements that have an absolute size.
@@ -952,26 +983,15 @@ find_panel_ranges <- function(g, pixelratio, res) {
layout <- layout[order(layout$t, layout$l), ]
layout$panel <- seq_len(nrow(layout))
# When using a HiDPI client on a Linux server, the pixel
# dimensions are doubled, so we have to divide the dimensions by
# `pixelratio`. When a HiDPI client is used on a Mac server (with
# the quartz device), the pixel dimensions _aren't_ doubled, even though
# the image has double size. In the latter case we don't have to scale the
# numbers down.
pix_ratio <- 1
if (!grepl("^quartz", names(grDevices::dev.cur()))) {
pix_ratio <- pixelratio
}
# Return list of lists, where each inner list has left, right, top, bottom
# values for a panel
lapply(seq_len(nrow(layout)), function(i) {
p <- layout[i, , drop = FALSE]
list(
left = x_pos[p$l - 1] / pix_ratio,
right = x_pos[p$r] / pix_ratio,
bottom = y_pos[p$b] / pix_ratio,
top = y_pos[p$t - 1] / pix_ratio
left = x_pos[p$l - 1],
right = x_pos[p$r],
bottom = y_pos[p$b],
top = y_pos[p$t - 1]
)
})
}

View File

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

View File

@@ -419,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)) {
@@ -576,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(
@@ -591,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
@@ -770,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))

View File

@@ -445,6 +445,8 @@ ShinySession <- R6Class(
testMode = FALSE, # Are we running in test mode?
testExportExprs = list(),
outputValues = list(), # Saved output values (for testing mode)
currentOutputName = NULL, # Name of the currently-running output
outputInfo = list(), # List of information for each output
testSnapshotUrl = character(0),
sendResponse = function(requestMsg, value) {
@@ -491,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
@@ -691,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,
@@ -725,6 +738,8 @@ ShinySession <- R6Class(
private$.outputs <- list()
private$.outputOptions <- list()
self$cache <- MemoryCache$new()
private$bookmarkCallbacks <- Callbacks$new()
private$bookmarkedCallbacks <- Callbacks$new()
private$restoreCallbacks <- Callbacks$new()
@@ -901,9 +916,11 @@ ShinySession <- R6Class(
# Create subdir for this scope
if (!is.null(state$dir)) {
scopeState$dir <- file.path(state$dir, namespace)
res <- dir.create(scopeState$dir)
if (res == FALSE) {
stop("Error creating subdirectory for scope ", namespace)
if (!dirExists(scopeState$dir)) {
res <- dir.create(scopeState$dir)
if (res == FALSE) {
stop("Error creating subdirectory for scope ", namespace)
}
}
}
@@ -961,8 +978,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) {
@@ -1070,7 +1088,11 @@ ShinySession <- R6Class(
# to include the $then/$catch calls below?
hybrid_chain(
hybrid_chain(
shinyCallingHandlers(func()),
{
private$withCurrentOutput(name, {
shinyCallingHandlers(func())
})
},
catch = function(cond) {
if (inherits(cond, "shiny.custom.error")) {
if (isTRUE(getOption("show.error.messages"))) printError(cond)
@@ -1313,6 +1335,47 @@ 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.
@@ -2056,6 +2119,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.
@@ -2124,7 +2197,9 @@ flushPendingSessions <- function() {
#' called from within the server function, this will default to the current
#' session, and the callback will be invoked when the current session ends. If
#' \code{onStop} is called outside a server function, then the callback will
#' be invoked with the application exits.
#' 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
@@ -2184,7 +2259,7 @@ flushPendingSessions <- 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

View File

@@ -1,4 +1,4 @@
globalVariables('func')
utils::globalVariables('func')
#' Mark a function as a render function
#'
@@ -118,7 +118,7 @@ useRenderFunction <- function(renderFunc, inline = FALSE) {
# 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)
@@ -451,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 \code{\link{uiOutput}}
#' @seealso \code{\link{uiOutput}}
#' @export
#' @examples
#' ## Only run examples in interactive R sessions

View File

@@ -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
@@ -643,10 +643,22 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
return(updateSelectInput(session, inputId, label, choices, selected))
}
# convert a single vector to a data frame so it returns {label: , value: }
# other objects return arbitrary JSON {x: , y: , foo: , ...}
choices <- if (is.atomic(choices)) {
# fast path
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 {
@@ -659,8 +671,52 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
}
data.frame(label = lab, value = choices, stringsAsFactors = FALSE)
} else {
# slow path
as.data.frame(choices, stringsAsFactors = FALSE)
# 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)
@@ -673,6 +729,43 @@ 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)

View File

@@ -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.
@@ -1679,14 +1698,14 @@ 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(...)
}
},
@@ -1695,7 +1714,7 @@ createVarPromiseDomain <- function(env, name, value) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
onRejected(...)
}
},
@@ -1707,4 +1726,17 @@ createVarPromiseDomain <- function(env, name, value) {
force(expr)
}
)
}
}
getSliderType <- function(min, max, value) {
vals <- dropNulls(list(value, min, max))
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
}
type[[1]]
}

View File

@@ -41,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",
@@ -195,7 +197,9 @@ sd_section("Utility functions",
"exprToFunction",
"installExprFunction",
"parseQueryString",
"getCurrentOutputInfo",
"plotPNG",
"sizeGrowthRatio",
"exportTestValues",
"setSerializer",
"snapshotExclude",
@@ -206,7 +210,10 @@ sd_section("Utility functions",
"shinyDeprecated",
"serverInfo",
"shiny-options",
"onStop"
"onStop",
"diskCache",
"memoryCache",
"key_missing"
)
)
sd_section("Plot interaction",

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,13 +5,13 @@
\alias{fixedPanel}
\title{Panel with absolute positioning}
\usage{
absolutePanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
width = NULL, height = NULL, draggable = FALSE, fixed = FALSE,
cursor = c("auto", "move", "default", "inherit"))
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

View File

@@ -62,6 +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}

View File

@@ -6,8 +6,8 @@
\usage{
bookmarkButton(label = "Bookmark...", icon = shiny::icon("link", lib =
"glyphicon"),
title = "Bookmark this application's state and get a URL for sharing.", ...,
id = "._bookmark_")
title = "Bookmark this application's state and get a URL for sharing.",
..., id = "._bookmark_")
}
\arguments{
\item{label}{The contents of the button or link--usually a text label, but

View File

@@ -4,9 +4,9 @@
\alias{brushOpts}
\title{Create an object representing brushing options}
\usage{
brushOpts(id = NULL, fill = "#9cf", stroke = "#036", opacity = 0.25,
delay = 300, delayType = c("debounce", "throttle"), clip = TRUE,
direction = c("xy", "x", "y"), resetOnNew = FALSE)
brushOpts(id = NULL, fill = "#9cf", stroke = "#036",
opacity = 0.25, delay = 300, delayType = c("debounce", "throttle"),
clip = TRUE, direction = c("xy", "x", "y"), resetOnNew = FALSE)
}
\arguments{
\item{id}{Input value name. For example, if the value is \code{"plot_brush"},

View File

@@ -5,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,6 +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}

View File

@@ -46,6 +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}

View File

@@ -4,8 +4,8 @@
\alias{createRenderFunction}
\title{Implement render functions}
\usage{
createRenderFunction(func, transform = function(value, session, name, ...)
value, outputFunc = NULL, outputArgs = NULL)
createRenderFunction(func, transform = function(value, session, name,
...) value, outputFunc = NULL, outputArgs = NULL)
}
\arguments{
\item{func}{A function without parameters, that returns user data. If the

View File

@@ -107,6 +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}

View File

@@ -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, autoclose = TRUE)
max = NULL, format = "yyyy-mm-dd", startview = "month",
weekstart = 0, language = "en", separator = " to ", width = NULL,
autoclose = TRUE)
}
\arguments{
\item{inputId}{The \code{input} slot that will be used to access the value.}
@@ -124,6 +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}

View File

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

View File

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

View File

@@ -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,6 +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}

View File

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

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

View File

@@ -4,8 +4,8 @@
\alias{hoverOpts}
\title{Create an object representing hover options}
\usage{
hoverOpts(id = NULL, delay = 300, delayType = c("debounce", "throttle"),
clip = TRUE, nullOutside = TRUE)
hoverOpts(id = NULL, delay = 300, delayType = c("debounce",
"throttle"), clip = TRUE, nullOutside = TRUE)
}
\arguments{
\item{id}{Input value name. For example, if the value is \code{"plot_hover"},

View File

@@ -5,11 +5,11 @@
\alias{uiOutput}
\title{Create an HTML output element}
\usage{
htmlOutput(outputId, inline = FALSE, container = if (inline) span else div,
...)
htmlOutput(outputId, inline = FALSE, container = if (inline) span else
div, ...)
uiOutput(outputId, inline = FALSE, container = if (inline) span else div,
...)
uiOutput(outputId, inline = FALSE, container = if (inline) span else
div, ...)
}
\arguments{
\item{outputId}{output variable to read the value from}

View File

@@ -4,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
View 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
View 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}.
}
}
}

View File

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

View File

@@ -4,8 +4,8 @@
\alias{navlistPanel}
\title{Create a navigation list panel}
\usage{
navlistPanel(..., id = NULL, selected = NULL, well = TRUE, fluid = TRUE,
widths = c(4, 8))
navlistPanel(..., id = NULL, selected = NULL, well = TRUE,
fluid = TRUE, widths = c(4, 8))
}
\arguments{
\item{...}{\code{\link{tabPanel}} elements to include in the navlist}

View File

@@ -5,8 +5,8 @@
\title{Find rows of data that are near a click/hover/double-click}
\usage{
nearPoints(df, coordinfo, xvar = NULL, yvar = NULL, panelvar1 = NULL,
panelvar2 = NULL, threshold = 5, maxpoints = NULL, addDist = FALSE,
allRows = FALSE)
panelvar2 = NULL, threshold = 5, maxpoints = NULL,
addDist = FALSE, allRows = FALSE)
}
\arguments{
\item{df}{A data frame from which to select rows.}

View File

@@ -53,6 +53,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}

View File

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

View File

@@ -7,13 +7,15 @@
\usage{
observeEvent(eventExpr, handlerExpr, event.env = parent.frame(),
event.quoted = FALSE, handler.env = parent.frame(),
handler.quoted = FALSE, label = NULL, suspended = FALSE, priority = 0,
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
handler.quoted = FALSE, label = NULL, suspended = FALSE,
priority = 0, domain = getDefaultReactiveDomain(),
autoDestroy = TRUE, ignoreNULL = TRUE, ignoreInit = FALSE,
once = FALSE)
eventReactive(eventExpr, valueExpr, event.env = parent.frame(),
event.quoted = FALSE, value.env = parent.frame(), value.quoted = FALSE,
label = NULL, domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
event.quoted = FALSE, value.env = parent.frame(),
value.quoted = FALSE, label = NULL,
domain = getDefaultReactiveDomain(), ignoreNULL = TRUE,
ignoreInit = FALSE)
}
\arguments{

View File

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

View File

@@ -55,6 +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}

View File

@@ -5,15 +5,15 @@
\alias{imageOutput}
\title{Create an plot or image output element}
\usage{
imageOutput(outputId, width = "100\%", height = "400px", click = NULL,
dblclick = NULL, hover = NULL, hoverDelay = NULL,
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
inline = FALSE)
imageOutput(outputId, width = "100\%", height = "400px",
click = NULL, dblclick = NULL, hover = NULL, hoverDelay = NULL,
hoverDelayType = NULL, brush = NULL, clickId = NULL,
hoverId = NULL, inline = FALSE)
plotOutput(outputId, width = "100\%", height = "400px", click = NULL,
dblclick = NULL, hover = NULL, hoverDelay = NULL,
hoverDelayType = NULL, brush = NULL, clickId = NULL, hoverId = NULL,
inline = FALSE)
hoverDelayType = NULL, brush = NULL, clickId = NULL,
hoverId = NULL, inline = FALSE)
}
\arguments{
\item{outputId}{output variable to read the plot/image from.}

View File

@@ -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,6 +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}

316
man/renderCachedPlot.Rd Normal file
View File

@@ -0,0 +1,316 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/render-cached-plot.R
\name{renderCachedPlot}
\alias{renderCachedPlot}
\title{Plot output with cached images}
\usage{
renderCachedPlot(expr, cacheKeyExpr, sizePolicy = sizeGrowthRatio(width =
400, height = 400, growthRate = 1.2), res = 72, cache = "app", ...,
outputArgs = list())
}
\arguments{
\item{expr}{An expression that generates a plot.}
\item{cacheKeyExpr}{An expression that returns a cache key. This key should
be a unique identifier for a plot: the assumption is that if the cache key
is the same, then the plot will be the same.}
\item{sizePolicy}{A function that takes two arguments, \code{width} and
\code{height}, and returns a list with \code{width} and \code{height}. The
purpose is to round the actual pixel dimensions from the browser to some
other dimensions, so that this will not generate and cache images of every
possible pixel dimension. See \code{\link{sizeGrowthRatio}} for more
information on the default sizing policy.}
\item{res}{The resolution of the PNG, in pixels per inch.}
\item{cache}{The scope of the cache, or a cache object. This can be
\code{"app"} (the default), \code{"session"}, or a cache object like
a \code{\link{diskCache}}. See the Cache Scoping section for more
information.}
\item{...}{Arguments to be passed through to \code{\link[grDevices]{png}}.
These can be used to set the width, height, background color, etc.}
\item{outputArgs}{A list of arguments to be passed through to the implicit
call to \code{\link{plotOutput}} when \code{renderPlot} is used in an
interactive R Markdown document.}
}
\description{
Renders a reactive plot, with plot images cached to disk.
}
\details{
\code{expr} is an expression that generates a plot, similar to that in
\code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
take reactive dependencies. It is re-executed only when the cache key
changes.
\code{cacheKeyExpr} is an expression which, when evaluated, returns an object
which will be serialized and hashed using the \code{\link[digest]{digest}}
function to generate a string that will be used as a cache key. This key is
used to identify the contents of the plot: if the cache key is the same as a
previous time, it assumes that the plot is the same and can be retrieved from
the cache.
This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
upstream reactives are invalidated. This will also trigger re-execution of
the plotting expression, \code{expr}.
The key should consist of "normal" R objects, like vectors and lists. Lists
should in turn contain other normal R objects. If the key contains
environments, external pointers, or reference objects -- or even if it has
such objects attached as attributes -- then it is possible that it will
change unpredictably even when you do not expect it to. Additionally, because
the entire key is serialized and hashed, if it contains a very large object
-- a large data set, for example -- there may be a noticeable performance
penalty.
If you face these issues with the cache key, you can work around them by
extracting out the important parts of the objects, and/or by converting them
to normal R objects before returning them. Your expression could even
serialize and hash that information in an efficient way and return a string,
which will in turn be hashed (very quickly) by the
\code{\link[digest]{digest}} function.
Internally, the result from \code{cacheKeyExpr} is combined with the name of
the output (if you assign it to \code{output$plot1}, it will be combined
with \code{"plot1"}) to form the actual key that is used. As a result, even
if there are multiple plots that have the same \code{cacheKeyExpr}, they
will not have cache key collisions.
}
\section{Cache scoping}{
There are a number of different ways you may want to scope the cache. For
example, you may want each user session to have their own plot cache, or
you may want each run of the application to have a cache (shared among
possibly multiple simultaneous user sessions), or you may want to have a
cache that persists even after the application is shut down and started
again.
To control the scope of the cache, use the \code{cache} parameter. There
are two ways of having Shiny automatically create and clean up the disk
cache.
\describe{
\item{1}{To scope the cache to one run of a Shiny application (shared
among possibly multiple user sessions), use \code{cache="app"}. This
is the default. The cache will be shared across multiple sessions, so
there is potentially a large performance benefit if there are many users
of the application. When the application stops running, the cache will
be deleted. If plots cannot be safely shared across users, this should
not be used.}
\item{2}{To scope the cache to one session, use \code{cache="session"}.
When a new user session starts -- in other words, when a web browser
visits the Shiny application -- a new cache will be created on disk
for that session. When the session ends, the cache will be deleted.
The cache will not be shared across multiple sessions.}
}
If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
in size, and will be stored stored in memory, using a
\code{\link{memoryCache}} object. Note that the cache space will be shared
among all cached plots within a single application or session.
In some cases, you may want more control over the caching behavior. For
example, you may want to use a larger or smaller cache, share a cache
among multiple R processes, or you may want the cache to persist across
multiple runs of an application, or even across multiple R processes.
To use different settings for an application-scoped cache, you can call
\code{\link{shinyOptions}()} at the top of your app.R, server.R, or
global.R. For example, this will create a cache with 20 MB of space
instead of the default 10 MB:
\preformatted{
shinyOptions(cache = memoryCache(size = 20e6))
}
To use different settings for a session-scoped cache, you can call
\code{\link{shinyOptions}()} at the top of your server function. To use
the session-scoped cache, you must also call \code{renderCachedPlot} with
\code{cache="session"}. This will create a 20 MB cache for the session:
\preformatted{
function(input, output, session) {
shinyOptions(cache = memoryCache(size = 20e6))
output$plot <- renderCachedPlot(
...,
cache = "session"
)
}
}
If you want to create a cache that is shared across multiple concurrent
R processes, you can use a \code{\link{diskCache}}. You can create an
application-level shared cache by putting this at the top of your app.R,
server.R, or global.R:
\preformatted{
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
}
This will create a subdirectory in your system temp directory named
\code{myapp-cache} (replace \code{myapp-cache} with a unique name of
your choosing). On most platforms, this directory will be removed when
your system reboots. This cache will persist across multiple starts and
stops of the R process, as long as you do not reboot.
To have the cache persist even across multiple reboots, you can create the
cache in a location outside of the temp directory. For example, it could
be a subdirectory of the application:
\preformatted{
shinyOptions(cache = diskCache("./myapp-cache"))
}
In this case, resetting the cache will have to be done manually, by deleting
the directory.
You can also scope a cache to just one plot, or selected plots. To do that,
create a \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
as the \code{cache} argument of \code{renderCachedPlot}.
}
\section{Interactive plots}{
\code{renderCachedPlot} can be used to create interactive plots. See
\code{\link{plotOutput}} for more information and examples.
}
\examples{
## Only run examples in interactive R sessions
if (interactive()) {
# A basic example that uses the default app-scoped memory cache.
# The cache will be shared among all simultaneous users of the application.
shinyApp(
fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
),
mainPanel(plotOutput("plot"))
)
),
function(input, output, session) {
output$plot <- renderCachedPlot({
Sys.sleep(2) # Add an artificial delay
seqn <- seq_len(input$n)
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
},
cacheKeyExpr = { list(input$n) }
)
}
)
# An example uses a data object shared across sessions. mydata() is part of
# the cache key, so when its value changes, plots that were previously
# stored in the cache will no longer be used (unless mydata() changes back
# to its previous value).
mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 50, 400, 100, step = 50),
actionButton("newdata", "New data")
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
observeEvent(input$newdata, {
mydata(data.frame(x = rnorm(400), y = rnorm(400)))
})
output$plot <- renderCachedPlot(
{
Sys.sleep(2)
d <- mydata()
seqn <- seq_len(input$n)
plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
},
cacheKeyExpr = { list(input$n, mydata()) },
)
}
shinyApp(ui, server)
# A basic application with two plots, where each plot in each session has
# a separate cache.
shinyApp(
fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
),
mainPanel(
plotOutput("plot1"),
plotOutput("plot2")
)
)
),
function(input, output, session) {
output$plot1 <- renderCachedPlot({
Sys.sleep(2) # Add an artificial delay
seqn <- seq_len(input$n)
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
},
cacheKeyExpr = { list(input$n) },
cache = memoryCache()
)
output$plot2 <- renderCachedPlot({
Sys.sleep(2) # Add an artificial delay
seqn <- seq_len(input$n)
plot(mtcars$wt[seqn], mtcars$mpg[seqn],
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
},
cacheKeyExpr = { list(input$n) },
cache = memoryCache()
)
}
)
}
\dontrun{
# At the top of app.R, this set the application-scoped cache to be a memory
# cache that is 20 MB in size, and where cached objects expire after one
# hour.
shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
# At the top of app.R, this set the application-scoped cache to be a disk
# cache that can be shared among multiple concurrent R processes, and is
# deleted when the system reboots.
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
# At the top of app.R, this set the application-scoped cache to be a disk
# cache that can be shared among multiple concurrent R processes, and
# persists on disk across reboots.
shinyOptions(cache = diskCache("./myapp-cache"))
# At the top of the server function, this set the session-scoped cache to be
# a memory cache that is 5 MB in size.
server <- function(input, output, session) {
shinyOptions(cache = memoryCache(max_size = 5e6))
output$plot <- renderCachedPlot(
...,
cache = "session"
)
}
}
}
\seealso{
See \code{\link{renderPlot}} for the regular, non-cached version of
this function. For more about configuring caches, see
\code{\link{memoryCache}} and \code{\link{diskCache}}.
}

View File

@@ -5,8 +5,8 @@
\title{Table output with the JavaScript library DataTables}
\usage{
renderDataTable(expr, options = NULL, searchDelay = 500,
callback = "function(oTable) {}", escape = TRUE, env = parent.frame(),
quoted = FALSE, outputArgs = list())
callback = "function(oTable) {}", escape = TRUE,
env = parent.frame(), quoted = FALSE, outputArgs = list())
}
\arguments{
\item{expr}{An expression that returns a data frame or a matrix.}

View File

@@ -4,8 +4,8 @@
\alias{renderImage}
\title{Image file output}
\usage{
renderImage(expr, env = parent.frame(), quoted = FALSE, deleteFile = TRUE,
outputArgs = list())
renderImage(expr, env = parent.frame(), quoted = FALSE,
deleteFile = TRUE, outputArgs = list())
}
\arguments{
\item{expr}{An expression that returns a list.}

View File

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

View File

@@ -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,6 +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}

View File

@@ -13,8 +13,8 @@
\alias{as.tags.shiny.appobj}
\title{Create a Shiny app object}
\usage{
shinyApp(ui = NULL, server = NULL, onStart = NULL, options = list(),
uiPattern = "/", enableBookmarking = NULL)
shinyApp(ui = NULL, server = NULL, onStart = NULL,
options = list(), uiPattern = "/", enableBookmarking = NULL)
shinyAppDir(appDir, options = list())

33
man/sizeGrowthRatio.Rd Normal file
View 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}}.
}

View File

@@ -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,6 +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}

View File

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

View File

@@ -72,6 +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}

View File

@@ -4,8 +4,8 @@
\alias{tabsetPanel}
\title{Create a tabset panel}
\usage{
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs", "pills"),
position = NULL)
tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs",
"pills"), position = NULL)
}
\arguments{
\item{...}{\code{\link{tabPanel}} elements to include in the tabset}

View File

@@ -4,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,6 +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}

View File

@@ -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,6 +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}

View File

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

View File

@@ -4,9 +4,9 @@
\alias{updateCheckboxGroupInput}
\title{Change the value of a checkbox group input on the client}
\usage{
updateCheckboxGroupInput(session, inputId, label = NULL, choices = NULL,
selected = NULL, inline = FALSE, choiceNames = NULL,
choiceValues = NULL)
updateCheckboxGroupInput(session, inputId, label = NULL,
choices = NULL, selected = NULL, inline = FALSE,
choiceNames = NULL, choiceValues = NULL)
}
\arguments{
\item{session}{The \code{session} object passed to function given to

View File

@@ -4,8 +4,8 @@
\alias{updateDateInput}
\title{Change the value of a date input on the client}
\usage{
updateDateInput(session, inputId, label = NULL, value = NULL, min = NULL,
max = NULL)
updateDateInput(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL)
}
\arguments{
\item{session}{The \code{session} object passed to function given to

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,18 @@ $.extend(selectInputBinding, {
find: function(scope) {
return $(scope).find('select');
},
getType: function(el) {
var $el = $(el);
if (!$el.hasClass("symbol")) {
// default character type
return null;
}
if ($el.attr("multiple") === "multiple") {
return 'shiny.symbolList';
} else {
return 'shiny.symbol';
}
},
getId: function(el) {
return InputBinding.prototype.getId.call(this, el) || el.name;
},
@@ -55,7 +67,7 @@ $.extend(selectInputBinding, {
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({
@@ -72,9 +84,19 @@ $.extend(selectInputBinding, {
callback();
},
success: function(res) {
// res = [{label: '1', value: '1', group: '1'}, ...]
// success is called after options are added, but
// groups need to be added manually below
$.each(res, function(index, elem) {
selectize.addOptionGroup(elem.group, { group: elem.group });
});
callback(res);
if (!loaded && data.hasOwnProperty('value'))
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;
}
});
@@ -111,7 +133,10 @@ $.extend(selectInputBinding, {
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') {

View File

@@ -6,6 +6,38 @@ function forceIonSliderUpdate(slider) {
console.log("Couldn't force ion slider to update");
}
function getTypePrettifyer(dataType, timeFormat, timezone) {
var timeFormatter;
var prettify;
if (dataType === 'date') {
timeFormatter = strftime.utc();
prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else if (dataType === 'datetime') {
if (timezone)
timeFormatter = strftime.timezone(timezone);
else
timeFormatter = strftime;
prettify = function(num) {
return timeFormatter(timeFormat, new Date(num));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
prettify = function(num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
return prettify;
}
var sliderInputBinding = {};
$.extend(sliderInputBinding, textInputBinding, {
find: function(scope) {
@@ -90,13 +122,31 @@ $.extend(sliderInputBinding, textInputBinding, {
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);
@@ -118,36 +168,9 @@ $.extend(sliderInputBinding, textInputBinding, {
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));
};
} else {
// The default prettify function for ion.rangeSlider adds thousands
// separators after the decimal mark, so we have our own version here.
// (#1958)
opts.prettify = function(num) {
// When executed, `this` will refer to the `IonRangeSlider.options`
// object.
return formatNumber(num, this.prettify_separator);
};
}
opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone);
$el.ionRangeSlider(opts);
},

File diff suppressed because it is too large Load Diff

View File

@@ -249,11 +249,17 @@ function mapValues(obj, f) {
const newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key))
newObj[key] = f(obj[key]);
newObj[key] = f(obj[key], key, obj);
}
return newObj;
}
// This is does the same as Number.isNaN, but that function unfortunately does
// not exist in any version of IE.
function isnan(x) {
return typeof(x) === 'number' && isNaN(x);
}
// Binary equality function used by the equal function.
function _equal(x, y) {
if ($.type(x) === "object" && $.type(y) === "object") {

View File

@@ -0,0 +1,84 @@
context("Cache")
test_that("DiskCache: handling missing values", {
d <- diskCache()
expect_true(is.key_missing(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL), NULL)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- diskCache(missing = NULL)
expect_true(is.null(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = -1), -1)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- diskCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
expect_error(d$get("abcd"), "^Missing key: abcd$")
# When exec_missing=TRUE, should be able to set a value that's identical to
# missing. Need to suppress warnings, because it will warn about reference
# object (the environment captured by the function)
d$set("x", NULL)
suppressWarnings(d$set("x", function(key) stop("Missing key: ", key)))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
expect_identical(d$get("y", exec_missing = FALSE), function(key) stop("Missing key: ", key))
expect_error(
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
"^Missing key 2: y$",
)
# Can't use exec_missing when missing is not a function
expect_error(diskCache(missing = 1, exec_missing = TRUE))
})
test_that("MemoryCache: handling missing values", {
d <- memoryCache()
expect_true(is.key_missing(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL), NULL)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- memoryCache(missing = NULL)
expect_true(is.null(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = -1), -1)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- memoryCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
expect_error(d$get("abcd"), "^Missing key: abcd$")
# When exec_missing==TRUE, should be able to set a value that's identical to
# missing.
d$set("x", NULL)
d$set("x", function(key) stop("Missing key: ", key))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
expect_error(
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
"^Missing key 2: y$",
)
# Can't create a cache with both missing and missing_f
expect_error(memoryCache(missing = 1, exec_missing = TRUE))
})

View File

@@ -48,3 +48,34 @@ test_that("Nulls are not converted to NAs in parsing", {
list(method="init", data=list(obs=500L, nullObs=NULL))
)
})
test_that("characters turn into symbols", {
handler <- inputHandlers$get("shiny.symbol")
x <- "mpg"
expect_identical(
handler(x),
as.symbol(x)
)
expect_identical(
handler(NULL),
NULL
)
})
test_that("character vectors turn into symbol lists", {
handler <- inputHandlers$get("shiny.symbolList")
x <- list("mpg")
expect_identical(
handler(x),
list(as.symbol(x[[1]]))
)
x <- list("mpg", "cyl", "disp")
expect_identical(
handler(x),
list(as.symbol(x[[1]]), as.symbol(x[[2]]), as.symbol(x[[3]]))
)
expect_identical(
handler(NULL),
list()
)
})

View File

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

View File

@@ -21,50 +21,52 @@ test_that("ggplot coordmap", {
scale_x_continuous(expand = c(0, 0)) +
scale_y_continuous(expand = c(0, 0))
png(tmpfile, width = 500, height = 500)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 500, 72)
dev.off()
expect_equal(m$dims, list(width = 500, height = 500))
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar"))
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar"))
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=5, bottom=10, top=20))
)
# Check for no log bases
expect_equal(
sortList(m[[1]]$log),
sortList(m$panels[[1]]$log),
sortList(list(x=NULL, y=NULL))
)
# panel_vars should be an empty named list
expect_identical(m[[1]]$panel_vars, list(a=1)[0])
expect_identical(m$panels[[1]]$panel_vars, list(a=1)[0])
# Sanity check for ranges. Checking exact range values isn't feasible due to
# variations in graphics devices, and possible changes to positioning in
# ggplot2.
expect_true(m[[1]]$range$left > 20 && m[[1]]$range$left < 70)
expect_true(m[[1]]$range$right > 480 && m[[1]]$range$right < 499)
expect_true(m[[1]]$range$bottom > 450 && m[[1]]$range$bottom < 490)
expect_true(m[[1]]$range$top > 1 && m[[1]]$range$top < 20)
expect_true(m$panels[[1]]$range$left > 20 && m$panels[[1]]$range$left < 70)
expect_true(m$panels[[1]]$range$right > 480 && m$panels[[1]]$range$right < 499)
expect_true(m$panels[[1]]$range$bottom > 450 && m$panels[[1]]$range$bottom < 490)
expect_true(m$panels[[1]]$range$top > 1 && m$panels[[1]]$range$top < 20)
# Scatterplot where aes() is declared in geom
p <- ggplot(dat, aes(xvar)) + geom_point(aes(y=yvar))
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 500, 72)
dev.off()
# Check mapping vars
expect_equal(sortList(m[[1]]$mapping), list(x = "xvar", y = "yvar"))
expect_equal(sortList(m$panels[[1]]$mapping), list(x = "xvar", y = "yvar"))
# Plot with an expression in aes, and a computed variable (histogram)
p <- ggplot(dat, aes(xvar/2)) + geom_histogram(binwidth=1)
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 500, 72)
dev.off()
# Check mapping vars - no value for y
expect_equal(sortList(m[[1]]$mapping), list(x = "xvar/2", y = NULL))
expect_equal(sortList(m$panels[[1]]$mapping), list(x = "xvar/2", y = NULL))
})
@@ -81,38 +83,38 @@ test_that("ggplot coordmap with facet_wrap", {
scale_y_continuous(expand = c(0, 0)) +
facet_wrap(~ g, ncol = 2)
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Should have 3 panels
expect_equal(length(m), 3)
expect_equal(m[[1]]$panel, 1)
expect_equal(m[[1]]$row, 1)
expect_equal(m[[1]]$col, 1)
expect_equal(m[[2]]$panel, 2)
expect_equal(m[[2]]$row, 1)
expect_equal(m[[2]]$col, 2)
expect_equal(m[[3]]$panel, 3)
expect_equal(m[[3]]$row, 2)
expect_equal(m[[3]]$col, 1)
expect_equal(length(m$panels), 3)
expect_equal(m$panels[[1]]$panel, 1)
expect_equal(m$panels[[1]]$row, 1)
expect_equal(m$panels[[1]]$col, 1)
expect_equal(m$panels[[2]]$panel, 2)
expect_equal(m$panels[[2]]$row, 1)
expect_equal(m$panels[[2]]$col, 2)
expect_equal(m$panels[[3]]$panel, 3)
expect_equal(m$panels[[3]]$row, 2)
expect_equal(m$panels[[3]]$col, 1)
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=10, bottom=10, top=30))
)
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
# Check panel vars
factor_vals <- dat$g
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
})
@@ -130,75 +132,75 @@ test_that("ggplot coordmap with facet_grid", {
# facet_grid horizontal
p1 <- p + facet_grid(. ~ g)
png(tmpfile)
m <- getGgplotCoordmap(print(p1), 1, 72)
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
dev.off()
# Should have 3 panels
expect_equal(length(m), 3)
expect_equal(m[[1]]$panel, 1)
expect_equal(m[[1]]$row, 1)
expect_equal(m[[1]]$col, 1)
expect_equal(m[[2]]$panel, 2)
expect_equal(m[[2]]$row, 1)
expect_equal(m[[2]]$col, 2)
expect_equal(m[[3]]$panel, 3)
expect_equal(m[[3]]$row, 1)
expect_equal(m[[3]]$col, 3)
expect_equal(length(m$panels), 3)
expect_equal(m$panels[[1]]$panel, 1)
expect_equal(m$panels[[1]]$row, 1)
expect_equal(m$panels[[1]]$col, 1)
expect_equal(m$panels[[2]]$panel, 2)
expect_equal(m$panels[[2]]$row, 1)
expect_equal(m$panels[[2]]$col, 2)
expect_equal(m$panels[[3]]$panel, 3)
expect_equal(m$panels[[3]]$row, 1)
expect_equal(m$panels[[3]]$col, 3)
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=10, bottom=10, top=30))
)
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
# Check panel vars
factor_vals <- dat$g
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
# facet_grid vertical
p1 <- p + facet_grid(g ~ .)
png(tmpfile)
m <- getGgplotCoordmap(print(p1), 1, 72)
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
dev.off()
# Should have 3 panels
expect_equal(length(m), 3)
expect_equal(m[[1]]$panel, 1)
expect_equal(m[[1]]$row, 1)
expect_equal(m[[1]]$col, 1)
expect_equal(m[[2]]$panel, 2)
expect_equal(m[[2]]$row, 2)
expect_equal(m[[2]]$col, 1)
expect_equal(m[[3]]$panel, 3)
expect_equal(m[[3]]$row, 3)
expect_equal(m[[3]]$col, 1)
expect_equal(length(m$panels), 3)
expect_equal(m$panels[[1]]$panel, 1)
expect_equal(m$panels[[1]]$row, 1)
expect_equal(m$panels[[1]]$col, 1)
expect_equal(m$panels[[2]]$panel, 2)
expect_equal(m$panels[[2]]$row, 2)
expect_equal(m$panels[[2]]$col, 1)
expect_equal(m$panels[[3]]$panel, 3)
expect_equal(m$panels[[3]]$row, 3)
expect_equal(m$panels[[3]]$col, 1)
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "g"))
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=10, bottom=10, top=30))
)
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
# Check panel vars
factor_vals <- dat$g
expect_equal(m[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = factor_vals[1]))
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = factor_vals[2]))
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = factor_vals[3]))
})
@@ -215,43 +217,43 @@ test_that("ggplot coordmap with 2D facet_grid", {
p1 <- p + facet_grid(g ~ h)
png(tmpfile)
m <- getGgplotCoordmap(print(p1), 1, 72)
m <- getGgplotCoordmap(print(p1), 500, 400, 72)
dev.off()
# Should have 4 panels
expect_equal(length(m), 4)
expect_equal(m[[1]]$panel, 1)
expect_equal(m[[1]]$row, 1)
expect_equal(m[[1]]$col, 1)
expect_equal(m[[2]]$panel, 2)
expect_equal(m[[2]]$row, 1)
expect_equal(m[[2]]$col, 2)
expect_equal(m[[3]]$panel, 3)
expect_equal(m[[3]]$row, 2)
expect_equal(m[[3]]$col, 1)
expect_equal(m[[4]]$panel, 4)
expect_equal(m[[4]]$row, 2)
expect_equal(m[[4]]$col, 2)
expect_equal(length(m$panels), 4)
expect_equal(m$panels[[1]]$panel, 1)
expect_equal(m$panels[[1]]$row, 1)
expect_equal(m$panels[[1]]$col, 1)
expect_equal(m$panels[[2]]$panel, 2)
expect_equal(m$panels[[2]]$row, 1)
expect_equal(m$panels[[2]]$col, 2)
expect_equal(m$panels[[3]]$panel, 3)
expect_equal(m$panels[[3]]$row, 2)
expect_equal(m$panels[[3]]$col, 1)
expect_equal(m$panels[[4]]$panel, 4)
expect_equal(m$panels[[4]]$row, 2)
expect_equal(m$panels[[4]]$col, 2)
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "h", panelvar2 = "g"))
expect_equal(m[[1]]$mapping, m[[2]]$mapping)
expect_equal(m[[2]]$mapping, m[[3]]$mapping)
expect_equal(m[[4]]$mapping, m[[4]]$mapping)
expect_equal(m$panels[[1]]$mapping, list(x = "xvar", y = "yvar", panelvar1 = "h", panelvar2 = "g"))
expect_equal(m$panels[[1]]$mapping, m$panels[[2]]$mapping)
expect_equal(m$panels[[2]]$mapping, m$panels[[3]]$mapping)
expect_equal(m$panels[[4]]$mapping, m$panels[[4]]$mapping)
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=15, bottom=10, top=40))
)
expect_equal(sortList(m[[1]]$domain), sortList(m[[2]]$domain))
expect_equal(sortList(m[[2]]$domain), sortList(m[[3]]$domain))
expect_equal(sortList(m[[3]]$domain), sortList(m[[4]]$domain))
expect_equal(sortList(m$panels[[1]]$domain), sortList(m$panels[[2]]$domain))
expect_equal(sortList(m$panels[[2]]$domain), sortList(m$panels[[3]]$domain))
expect_equal(sortList(m$panels[[3]]$domain), sortList(m$panels[[4]]$domain))
# Check panel vars
expect_equal(m[[1]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[1]))
expect_equal(m[[2]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[1]))
expect_equal(m[[3]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[2]))
expect_equal(m[[4]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[2]))
expect_equal(m$panels[[1]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[1]))
expect_equal(m$panels[[2]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[1]))
expect_equal(m$panels[[3]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[2]))
expect_equal(m$panels[[4]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[2]))
})
@@ -265,12 +267,12 @@ test_that("ggplot coordmap with various data types", {
scale_x_discrete(expand = c(0 ,0)) +
scale_y_discrete(expand = c(0, 0))
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=1, right=3, bottom=1, top=4))
)
@@ -283,12 +285,12 @@ test_that("ggplot coordmap with various data types", {
scale_x_date(expand = c(0 ,0)) +
scale_y_datetime(expand = c(0, 0))
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Check domain
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(
left = as.numeric(dat$xvar[1]),
right = as.numeric(dat$xvar[2]),
@@ -308,12 +310,12 @@ test_that("ggplot coordmap with various scales and coords", {
scale_x_continuous(expand = c(0 ,0)) +
scale_y_reverse(expand = c(0, 0))
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Check domain (y reversed)
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=0, right=5, bottom=20, top=10))
)
@@ -323,14 +325,14 @@ test_that("ggplot coordmap with various scales and coords", {
scale_y_continuous(expand = c(0 ,0)) +
coord_flip()
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Check mapping vars
expect_equal(m[[1]]$mapping, list(x = "yvar", y = "xvar"))
expect_equal(m$panels[[1]]$mapping, list(x = "yvar", y = "xvar"))
# Check domain (y reversed)
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=10, right=20, bottom=0, top=5))
)
@@ -341,17 +343,17 @@ test_that("ggplot coordmap with various scales and coords", {
scale_y_continuous(expand = c(0, 0)) +
coord_trans(y = "log2")
png(tmpfile)
m <- getGgplotCoordmap(print(p), 1, 72)
m <- getGgplotCoordmap(print(p), 500, 400, 72)
dev.off()
# Check log bases
expect_equal(
sortList(m[[1]]$log),
sortList(m$panels[[1]]$log),
sortList(list(x=10, y=2))
)
# Check domains
expect_equal(
sortList(m[[1]]$domain),
sortList(m$panels[[1]]$domain),
sortList(list(left=-1, right=3, bottom=-2, top=4))
)
})

View File

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

View File

@@ -81,7 +81,7 @@ module.exports = function(grunt) {
},
options: {
replacements: [{
pattern: /{{ VERSION }}/g,
pattern: /{{\s*VERSION\s*}}/g,
replacement: pkgInfo().version
}]
}
@@ -157,11 +157,7 @@ module.exports = function(grunt) {
shiny: {
files: ['<%= concat.shiny.src %>', '../DESCRIPTION'],
tasks: [
'newer:concat',
'newer:eslint',
'configureBabel',
'newer:babel',
'newer:uglify'
'default'
]
},
datepicker: {
@@ -202,11 +198,23 @@ module.exports = function(grunt) {
gruntConfig.babel.options.inputSourceMap = grunt.file.readJSON('./temp_concat/shiny.js.map');
});
grunt.task.registerTask(
"validateStringReplace",
"tests to make sure the version value was replaced",
function() {
var shinyContent = require('fs').readFileSync('./temp_concat/shiny.js', 'utf8');
if (/{{\s*VERSION\s*}}/.test(shinyContent)) {
grunt.fail.fatal("{{ VERSION }} was not replaced in compiled shiny.js file!")
}
}
);
grunt.initConfig(gruntConfig);
grunt.registerTask('default', [
'newer:concat',
'newer:string-replace',
'validateStringReplace',
'newer:eslint',
'configureBabel',
'newer:babel',