Compare commits

...

54 Commits

Author SHA1 Message Date
Winston Chang
f089531bd1 Put man-roxygen in .Rbuildignore 2013-08-27 10:08:03 -05:00
Winston Chang
8d8ea53804 Fixes to imports for R-devel 2013-08-26 14:19:50 -05:00
Winston Chang
89e405e927 Bump version to 0.7.0 2013-08-26 12:03:02 -05:00
Joe Cheng
ca984a6630 Implement shiny.sharedSecret option 2013-08-25 22:20:38 -07:00
Joe Cheng
fa39a55eca Wow, IE10 is *really* picky about websocket URLs 2013-08-23 15:42:19 -07:00
Joe Cheng
c3a1ba2f2d Make websocket URL work with IE10
See https://github.com/einaros/ws/issues/131#issuecomment-15715373
2013-08-23 15:38:12 -07:00
Joe Cheng
86e291f250 Add docs for showReactLog. Un-export writeReactLog. 2013-08-23 13:11:04 -07:00
Jeff Allen
dd1d4439a9 Merge pull request #218 from wch/fix-log
Cleanups to reactive logging code for R CMD check
2013-08-23 12:16:23 -07:00
Joe Cheng
cbfde18f8c More updates to NEWS 2013-08-23 12:03:11 -07:00
Winston Chang
e2c2e23d2a Cleanups to reactive logging code for R CMD check
* Fix 'env' partial argument match for get().
* Fix unbound variable access of .shiny__stdout.
* Restructure if statement so test is only done once.
2013-08-23 12:16:18 -05:00
Winston Chang
40cc5d5242 Doc fixes for R CMD check 2013-08-23 12:07:34 -05:00
Winston Chang
9765194ace Bump httpuv version dependency to 1.1.0 2013-08-23 10:16:47 -05:00
Winston Chang
628465e6b5 Update NEWS 2013-08-23 10:16:12 -05:00
Winston Chang
58706df120 Update sliderInput docs 2013-08-23 10:16:05 -05:00
Joe Cheng
b19225c747 Don't send websocket subprotocol
Some time recently, Google Chrome started actually caring what the
client sends for this and what the server replies with. httpuv
doesn't currently have any logic for subprotocol selection, so
it always replies with no Sec-WebSocket-Protocol header, which
now Google Chrome reacts to by closing the websocket connection.

This problem goes away if we just don't send a subprotocol at all.
2013-08-23 02:15:54 -07:00
Joe Cheng
c304889e61 Merge pull request #208 from trestletech/master
Add reactive timing logging, if the necessary variables are available.
2013-08-22 13:55:11 -07:00
trestletech
05a9204678 Added timestamp to reactive log. 2013-08-22 11:20:26 -05:00
hadley wickham
1a6901c3e3 Document range slider 2013-08-14 13:25:42 -05:00
Winston Chang
7aaba8244b Add is.reactivevalues function 2013-08-05 14:02:50 -05:00
Winston Chang
8c45dcde88 Merge pull request #205 from hadley/master
Class output of reactive.
2013-08-05 12:02:14 -07:00
Winston Chang
6c155b04b2 Merge pull request #197 from wch/fix-style
Add compatibility wrapper for getComputedStyle in IE8
2013-07-27 17:43:01 -07:00
Winston Chang
cd8ad9a2ec Add compatibility wrapper for getComputedStyle in IE8 2013-07-27 19:41:56 -05:00
hadley
a5db7d0246 Class output of reactive.
Also add print method and test
2013-07-27 11:06:35 -05:00
trestletech
b84b467b96 Added logging of start/stop flushing of reactives using a param provided via HTTP headers. 2013-07-24 17:40:24 -04:00
trestletech
0812aaac88 Merge remote-tracking branch 'rstudio/master' 2013-07-24 08:59:34 -04:00
Joe Cheng
194d2f911e Changes to public-facing session object
- Change from list to environment. This enables the next feature:
- Make $request a promise. websocket$request breaks on some (earlier?) versions
  of httpuv. I'm not sure why this is but in the few hours since I submitted
  the websocket$request change we've had a number of complaints. This way the
  error only occurs if the app actually asks for session$request.
- Make the private session object accessible via `.impl`. Obviously any data or
  methods on the private session object are unsupported but they are there if
  you are desperate and don't mind possible future breakage.
2013-07-23 15:39:27 -07:00
Joe Cheng
e360b36b8a Make WS request available on session object 2013-07-22 15:15:27 -07:00
trestletech
b6f66dd287 Moved print out so we can put it where it belongs. 2013-07-16 08:01:22 -07:00
trestletech
0a4bb48cd3 Added output of process ID on app start. 2013-07-15 14:26:25 -07:00
Joe Cheng
15d62d4a91 Add instructions for installing from GitHub 2013-07-12 01:32:49 +02:00
Joe Cheng
5b13c44ef9 Fix isolate return value bug (issue #200) 2013-07-12 01:30:07 +02:00
Joe Cheng
0a4250f3b4 Merge pull request #193 from jcheng5/reactlog
React log
2013-07-09 00:58:53 -07:00
Joe Cheng
f79223ed58 Merge remote-tracking branch 'origin/master' into reactlog 2013-07-07 01:20:59 -07:00
Joe Cheng
2d28218a2a Allow Cmd+F3 to launch reactlog 2013-07-07 01:20:40 -07:00
Joe Cheng
35974f2ee1 Firefox scrubbing fix 2013-07-06 23:13:25 -07:00
Joe Cheng
1f73323fb9 reactlog: Support arbitrary temporal movement 2013-07-06 23:06:38 -07:00
Joe Cheng
a3d0736eec Use more obscure keyboard shortcut for reactlog 2013-07-06 18:27:10 -07:00
Joe Cheng
4bdd486c00 reactlog: Firefox compatibility; visual tweaks 2013-07-06 18:22:40 -07:00
Joe Cheng
c3895c9bd7 Configurable hover delay type (debounce/throttle) 2013-07-05 17:29:57 -07:00
Joe Cheng
e9ddd89b32 Simpler math 2013-07-05 15:08:42 -07:00
Joe Cheng
88a8f2d609 Fix locator Retina compatibility. Again.
Was working locally on Macs but not on spark.rstudio.com accessed via rMBP.
2013-07-05 13:52:56 -07:00
Joe Cheng
a5dc5c89e8 Firefox locator compat 2013-07-05 12:30:14 -07:00
Joe Cheng
3a15a35137 Merge pull request #192 from jcheng5/flush-callbacks
Add session$onFlush and session$onFlushed
2013-07-05 02:10:02 -07:00
Joe Cheng
b644640804 Add session$onFlush and session$onFlushed 2013-07-05 02:07:51 -07:00
Joe Cheng
aaa4f66671 Merge pull request #183 from jcheng5/plot-mouse-events
Click and hover on static plots. Also, fix retina compatibility and make hover delay configurable
2013-07-04 23:43:54 -07:00
Joe Cheng
07e021199e Use crosshair cursor when plot supports hover/click 2013-07-04 23:42:44 -07:00
Joe Cheng
6b2ca7dc80 Merge pull request #182 from jcheng5/reactive-poll
Implement reactivePoll and reactiveFileReader
2013-07-04 23:31:16 -07:00
Joe Cheng
091d62803e Merge pull request #191 from trestletech/master
Restrict the number of observations to a valid, positive number.
2013-07-04 12:15:57 -07:00
trestletech
547999bae0 Restrict the number of observations to a valid, positive number. 2013-07-03 23:06:42 -05:00
Winston Chang
d403ec7399 Make hover delay configurable 2013-06-24 16:39:04 -05:00
Winston Chang
6ac77835df Fix Retina compatibility (revert b113119) 2013-06-24 16:31:43 -05:00
Joe Cheng
b113119a9a Retina display compatibility 2013-06-21 21:38:23 -07:00
Joe Cheng
b713057614 Implement click and hover events on static plots
plotOutput now takes clickId and hoverId params that tell Shiny
where to send click and hover events for that plot. The server.R
file can listen on input$<clickId> and/or input$<hoverId>. In
both cases, the resulting value will have numeric x and y elements
that indicate the mouse position in user coordinates. In the case
of hover events, it's also possible to have a NULL value which
means the mouse is not currently hovering over the plot.
2013-06-21 16:58:43 -07:00
Joe Cheng
d897df6a30 Implement reactivePoll and reactiveFileReader 2013-06-19 09:16:04 -07:00
24 changed files with 1221 additions and 80 deletions

View File

@@ -9,3 +9,4 @@
^\.gitignore$
^res$
^tools$
^man-roxygen$

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 0.6.0.99
Version: 0.7.0
Date: 2013-01-23
Author: RStudio, Inc.
Maintainer: Winston Chang <winston@rstudio.com>
@@ -16,14 +16,14 @@ Imports:
stats,
tools,
utils,
datasets,
methods,
httpuv (>= 1.0.6.2),
httpuv (>= 1.1.0),
caTools,
RJSONIO,
xtable,
digest
Suggests:
datasets,
markdown,
Cairo,
testthat

View File

@@ -17,6 +17,7 @@ S3method(as.list,reactivevalues)
S3method(format,shiny.tag)
S3method(format,shiny.tag.list)
S3method(names,reactivevalues)
S3method(print,reactive)
S3method(print,shiny.tag)
S3method(print,shiny.tag.list)
export(HTML)
@@ -57,6 +58,8 @@ export(includeMarkdown)
export(includeScript)
export(includeText)
export(invalidateLater)
export(is.reactive)
export(is.reactivevalues)
export(isolate)
export(mainPanel)
export(numericInput)
@@ -70,7 +73,9 @@ export(plotPNG)
export(pre)
export(radioButtons)
export(reactive)
export(reactiveFileReader)
export(reactivePlot)
export(reactivePoll)
export(reactivePrint)
export(reactiveTable)
export(reactiveText)
@@ -127,9 +132,9 @@ export(validateCssUnit)
export(verbatimTextOutput)
export(wellPanel)
export(withTags)
export(writeReactLog)
import(RJSONIO)
import(caTools)
import(digest)
import(httpuv)
import(methods)
import(xtable)

45
NEWS
View File

@@ -1,6 +1,49 @@
shiny 0.6.0.99
shiny 0.7.0
--------------------------------------------------------------------------------
* Stopped sending websocket subprotocol. This fixes a compatibility issue with
Google Chrome 30.
* The `input` and `output` objects are now also accessible via `session$input`
and `session$output`.
* Added click and hover events for static plots; see `?plotOutput` for details.
* Added optional logging of the execution states of a reactive program, and
tools for visualizing the log data. To use, start a new R session and call
`options(shiny.reactlog=TRUE)`. Then launch a Shiny app and interact with it.
Press Ctrl+F3 (or for Mac, Cmd+F3) in the browser to launch an interactive
visualization of the reactivity that has occurred. See `?showReactLog` for
more information.
* Added `includeScript()` and `includeCSS()` functions.
* Reactive expressions now have class="reactive" attribute. Also added
`is.reactive()` and `is.reactivevalues()` functions.
* New `stopApp()` function, which stops an app and returns a value to the caller
of `runApp()`.
* Added the `shiny.usecairo` option, which can be used to tell Shiny not to use
Cairo for PNG output even when it is installed. (Defaults to `TRUE`.)
* Speed increases for `selectInput()` and `radioButtons()`, and their
corresponding updater functions, for when they have many options.
* Added `tagSetChildren()` and `tagAppendChildren()` functions.
* The HTTP request object that created the websocket is now accessible from the
`session` object, as `session$request`. This is a Rook-like request
environment that can be used to access HTTP headers, among other things.
(Note: When running in a Shiny Server environment, the request will reflect
the proxy HTTP request that was made from the Shiny Server process to the R
process, not the request that was made from the web browser to Shiny Server.)
* Fix `getComputedStyle` issue, for IE8 browser compatibility (#196). Note:
Shiny Server is still required for IE8/9 compatibility.
* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret
to be set to the given value.
shiny 0.6.0
--------------------------------------------------------------------------------

View File

@@ -626,8 +626,10 @@ actionButton <- function(inputId, label) {
#' @param label A descriptive label to be displayed with the widget.
#' @param min The minimum value (inclusive) that can be selected.
#' @param max The maximum value (inclusive) that can be selected.
#' @param value The initial value of the slider. A warning will be issued if the
#' value doesn't fit between \code{min} and \code{max}.
#' @param value The initial value of the slider. A numeric vector of length
#' one will create a regular slider; a numeric vector of length two will
#' create a double-ended range slider.. A warning will be issued if the
#' value doesn't fit between \code{min} and \code{max}.
#' @param step Specifies the interval between each selectable value on the
#' slider (\code{NULL} means no restriction).
#' @param round \code{TRUE} to round all values to the nearest integer;
@@ -1093,6 +1095,24 @@ imageOutput <- function(outputId, width = "100%", height="400px") {
#' \code{"400px"}, \code{"auto"}) or a number, which will be coerced to a
#' string and have \code{"px"} appended.
#' @param height Plot height
#' @param clickId If not \code{NULL}, the plot will send coordinates to the
#' server whenever it is clicked. This information will be accessible on the
#' \code{input} object using \code{input$}\emph{\code{clickId}}. The value will be a
#' named list or vector with \code{x} and \code{y} elements indicating the
#' mouse position in user units.
#' @param hoverId If not \code{NULL}, the plot will send coordinates to the
#' server whenever the mouse pauses on the plot for more than the number of
#' milliseconds determined by \code{hoverTimeout}. This information will be
# accessible on the \code{input} object using \code{input$}\emph{\code{clickId}}.
#' The value will be \code{NULL} if the user is not hovering, and a named
#' list or vector with \code{x} and \code{y} elements indicating the mouse
#' position in user units.
#' @param hoverDelay The delay for hovering, in milliseconds.
#' @param hoverDelayType The type of algorithm for limiting the number of hover
#' events. Use \code{"throttle"} to limit the number of hover events to one
#' every \code{hoverDelay} milliseconds. Use \code{"debounce"} to suspend
#' events while the cursor is moving, and wait until the cursor has been at
#' rest for \code{hoverDelay} milliseconds before sending an event.
#' @return A plot output element that can be included in a panel
#' @examples
#' # Show a plot of the generated distribution
@@ -1100,10 +1120,23 @@ imageOutput <- function(outputId, width = "100%", height="400px") {
#' plotOutput("distPlot")
#' )
#' @export
plotOutput <- function(outputId, width = "100%", height="400px") {
plotOutput <- function(outputId, width = "100%", height="400px",
clickId = NULL, hoverId = NULL, hoverDelay = 300,
hoverDelayType = c("debounce", "throttle")) {
if (is.null(clickId) && is.null(hoverId)) {
hoverDelay <- NULL
hoverDelayType <- NULL
} else {
hoverDelayType <- match.arg(hoverDelayType)[[1]]
}
style <- paste("width:", validateCssUnit(width), ";",
"height:", validateCssUnit(height))
div(id = outputId, class = "shiny-plot-output", style = style)
div(id = outputId, class = "shiny-plot-output", style = style,
`data-click-id` = clickId,
`data-hover-id` = hoverId,
`data-hover-delay` = hoverDelay,
`data-hover-delay-type` = hoverDelayType)
}
#' Create a table output element

View File

@@ -1,8 +1,40 @@
#' @export
writeReactLog <- function(file=stdout()) {
cat(RJSONIO::toJSON(.graphEnv$log, pretty=TRUE), file=file)
}
#' Reactive Log Visualizer
#'
#' Provides an interactive browser-based tool for visualizing reactive
#' dependencies and execution in your application.
#'
#' To use the reactive log visualizer, start with a fresh R session and
#' run the command \code{options(shiny.reactlog=TRUE)}; then launch your
#' application in the usual way (e.g. using \code{\link{runApp}}). At
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
#' web browser to launch the reactive log visualization.
#'
#' The reactive log visualization only includes reactive activity up
#' until the time the report was loaded. If you want to see more recent
#' activity, refresh the browser.
#'
#' Note that Shiny does not distinguish between reactive dependencies
#' that "belong" to one Shiny user session versus another, so the
#' visualization will include all reactive activity that has taken place
#' in the process, not just for a particular application or session.
#'
#' As an alternative to pressing Ctrl/Command+F3--for example, if you
#' are using reactives outside of the context of a Shiny
#' application--you can run the \code{showReactLog} function, which will
#' generate the reactive log visualization as a static HTML file and
#' launch it in your default browser. In this case, refreshing your
#' browser will not load new activity into the report; you will need to
#' call \code{showReactLog()} explicitly.
#'
#' For security and performance reasons, do not enable
#' \code{shiny.reactlog} in production environments. When the option is
#' enabled, it's possible for any user of your app to see at least some
#' of the source code of your reactive expressions and observers.
#'
#' @export
showReactLog <- function() {
browseURL(renderReactLog())

View File

@@ -173,7 +173,7 @@ ReactiveValues <- setRefClass(
#' @param ... Objects that will be added to the reactivevalues object. All of
#' these objects must be named.
#'
#' @seealso \code{\link{isolate}}.
#' @seealso \code{\link{isolate}} and \code{\link{is.reactivevalues}}.
#'
#' @export
reactiveValues <- function(...) {
@@ -199,6 +199,15 @@ setOldClass("reactivevalues")
structure(list(impl=values), class='reactivevalues', readonly=readonly)
}
#' Checks whether an object is a reactivevalues object
#'
#' Checks whether its argument is a reactivevalues object.
#'
#' @param x The object to test.
#' @seealso \code{\link{reactiveValues}}.
#' @export
is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
#' @S3method $ reactivevalues
`$.reactivevalues` <- function(x, name) {
.subset2(x, 'impl')$get(name)
@@ -369,7 +378,8 @@ Observable <- setRefClass(
#' See the \href{http://rstudio.github.com/shiny/tutorial/}{Shiny tutorial} for
#' more information about reactive expressions.
#'
#' @param x An expression (quoted or unquoted).
#' @param x For \code{reactive}, an expression (quoted or unquoted). For
#' \code{is.reactive}, an object to test.
#' @param env The parent environment for the reactive expression. By default, this
#' is the calling environment, the same as when defining an ordinary
#' non-reactive expression.
@@ -377,6 +387,7 @@ Observable <- setRefClass(
#' This is useful when you want to use an expression that is stored in a
#' variable; to do so, it must be quoted with `quote()`.
#' @param label A label for the reactive expression, useful for debugging.
#' @return a function, wrapped in a S3 class "reactive"
#'
#' @examples
#' values <- reactiveValues(A=1)
@@ -403,9 +414,20 @@ reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL) {
if (is.null(label))
label <- sprintf('reactive(%s)', paste(deparse(body(fun)), collapse='\n'))
Observable$new(fun, label)$getValue
o <- Observable$new(fun, label)
structure(o$getValue@.Data, observable = o, class = "reactive")
}
#' @S3method print reactive
print.reactive <- function(x, ...) {
label <- attr(x, "observable")$.label
cat(label, "\n")
}
#' @export
#' @rdname reactive
is.reactive <- function(x) inherits(x, "reactive")
# Return the number of times that a reactive expression or observer has been run
execCount <- function(x) {
if (is.function(x))
@@ -743,6 +765,168 @@ invalidateLater <- function(millis, session) {
invisible()
}
coerceToFunc <- function(x) {
force(x);
if (is.function(x))
return(x)
else
return(function() x)
}
#' Reactive polling
#'
#' Used to create a reactive data source, which works by periodically polling a
#' non-reactive data source.
#'
#' \code{reactivePoll} works by pairing a relatively cheap "check" function with
#' a more expensive value retrieval function. The check function will be
#' executed periodically and should always return a consistent value until the
#' data changes. When the check function returns a different value, then the
#' value retrieval function will be used to re-populate the data.
#'
#' Note that the check function doesn't return \code{TRUE} or \code{FALSE} to
#' indicate whether the underlying data has changed. Rather, the check function
#' indicates change by returning a different value from the previous time it was
#' called.
#'
#' For example, \code{reactivePoll} is used to implement
#' \code{reactiveFileReader} by pairing a check function that simply returns the
#' last modified timestamp of a file, and a value retrieval function that
#' actually reads the contents of the file.
#'
#' As another example, one might read a relational database table reactively by
#' using a check function that does \code{SELECT MAX(timestamp) FROM table} and
#' a value retrieval function that does \code{SELECT * FROM table}.
#'
#' The \code{intervalMillis}, \code{checkFunc}, and \code{valueFunc} functions
#' will be executed in a reactive context; therefore, they may read reactive
#' values and reactive expressions.
#'
#' @param intervalMillis Approximate number of milliseconds to wait between
#' calls to \code{checkFunc}. This can be either a numeric value, or a
#' function that returns a numeric value.
#' @param session The user session to associate this file reader with, or
#' \code{NULL} if none. If non-null, the reader will automatically stop when
#' the session ends.
#' @param checkFunc A relatively cheap function whose values over time will be
#' tested for equality; inequality indicates that the underlying value has
#' changed and needs to be invalidated and re-read using \code{valueFunc}. See
#' Details.
#' @param valueFunc A function that calculates the underlying value. See
#' Details.
#'
#' @return A reactive expression that returns the result of \code{valueFunc},
#' and invalidates when \code{checkFunc} changes.
#'
#' @seealso \code{\link{reactiveFileReader}}
#'
#' @examples
#' \dontrun{
#' # Assume the existence of readTimestamp and readValue functions
#' shinyServer(function(input, output, session) {
#' data <- reactivePoll(1000, session, readTimestamp, readValue)
#' output$dataTable <- renderTable({
#' data()
#' })
#' })
#' }
#'
#' @export
reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
intervalMillis <- coerceToFunc(intervalMillis)
rv <- reactiveValues(cookie = isolate(checkFunc()))
observe({
rv$cookie <- checkFunc()
invalidateLater(intervalMillis(), session)
})
# TODO: what to use for a label?
re <- reactive({
rv$cookie
valueFunc()
}, label = NULL)
return(re)
}
#' Reactive file reader
#'
#' Given a file path and read function, returns a reactive data source for the
#' contents of the file.
#'
#' \code{reactiveFileReader} works by periodically checking the file's last
#' modified time; if it has changed, then the file is re-read and any reactive
#' dependents are invalidated.
#'
#' The \code{intervalMillis}, \code{filePath}, and \code{readFunc} functions
#' will each be executed in a reactive context; therefore, they may read
#' reactive values and reactive expressions.
#'
#' @param intervalMillis Approximate number of milliseconds to wait between
#' checks of the file's last modified time. This can be a numeric value, or a
#' function that returns a numeric value.
#' @param session The user session to associate this file reader with, or
#' \code{NULL} if none. If non-null, the reader will automatically stop when
#' the session ends.
#' @param filePath The file path to poll against and to pass to \code{readFunc}.
#' This can either be a single-element character vector, or a function that
#' returns one.
#' @param readFunc The function to use to read the file; must expect the first
#' argument to be the file path to read. The return value of this function is
#' used as the value of the reactive file reader.
#' @param ... Any additional arguments to pass to \code{readFunc} whenever it is
#' invoked.
#'
#' @return A reactive expression that returns the contents of the file, and
#' automatically invalidates when the file changes on disk (as determined by
#' last modified time).
#'
#' @seealso \code{\link{reactivePoll}}
#'
#' @examples
#' \dontrun{
#' # Per-session reactive file reader
#' shinyServer(function(input, output, session)) {
#' fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
#'
#' output$data <- renderTable({
#' fileData()
#' })
#' }
#'
#' # Cross-session reactive file reader. In this example, all sessions share
#' # the same reader, so read.csv only gets executed once no matter how many
#' # user sessions are connected.
#' fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
#' shinyServer(function(input, output, session)) {
#' output$data <- renderTable({
#' fileData()
#' })
#' }
#' }
#'
#' @export
reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...) {
filePath <- coerceToFunc(filePath)
extraArgs <- list(...)
reactivePoll(
intervalMillis, session,
function() {
path <- filePath()
info <- file.info(path)
return(paste(path, info$mtime, info$size))
},
function() {
do.call(readFunc, c(filePath(), extraArgs))
}
)
}
#' Create a non-reactive scope for an expression
#'
#' Executes the given expression in a scope where reactive values or expression
@@ -816,8 +1000,8 @@ invalidateLater <- function(millis, session) {
#' @export
isolate <- function(expr) {
ctx <- Context$new('[isolate]', type='isolate')
on.exit(ctx$invalidate())
ctx$run(function() {
expr
})
ctx$invalidate()
}

View File

@@ -12,7 +12,7 @@
#' @name shiny-package
#' @aliases shiny
#' @docType package
#' @import httpuv caTools RJSONIO xtable digest
#' @import httpuv caTools RJSONIO xtable digest methods
NULL
suppressPackageStartupMessages({
@@ -39,6 +39,8 @@ ShinySession <- setRefClass(
.input = 'ReactiveValues', # Internal object for normal input sent from client
.clientData = 'ReactiveValues', # Internal object for other data sent from the client
.closedCallbacks = 'Callbacks',
.flushCallbacks = 'Callbacks',
.flushedCallbacks = 'Callbacks',
input = 'reactivevalues', # Externally-usable S3 wrapper object for .input
output = 'ANY', # Externally-usable S3 wrapper object for .outputs
clientData = 'reactivevalues', # Externally-usable S3 wrapper object for .clientData
@@ -46,7 +48,7 @@ ShinySession <- setRefClass(
files = 'Map', # For keeping track of files sent to client
downloads = 'Map',
closed = 'logical',
session = 'list', # Object for the server app to access session stuff
session = 'environment', # Object for the server app to access session stuff
.workerId = 'character'
),
methods = list(
@@ -75,13 +77,21 @@ ShinySession <- setRefClass(
.outputs <<- list()
.outputOptions <<- list()
session <<- list(clientData = clientData,
sendCustomMessage = .self$.sendCustomMessage,
sendInputMessage = .self$.sendInputMessage,
onSessionEnded = .self$onSessionEnded,
isClosed = .self$isClosed,
input = .self$input,
output = .self$output)
session <<- new.env(parent=emptyenv())
session$clientData <<- clientData
session$sendCustomMessage <<- .self$.sendCustomMessage
session$sendInputMessage <<- .self$.sendInputMessage
session$onSessionEnded <<- .self$onSessionEnded
session$onFlush <<- .self$onFlush
session$onFlushed <<- .self$onFlushed
session$isClosed <<- .self$isClosed
session$input <<- .self$input
session$output <<- .self$output
session$.impl <<- .self
# session$request should throw an error if httpuv doesn't have
# websocket$request, but don't throw it until a caller actually
# tries to access session$request
delayedAssign('request', websocket$request, assign.env = session)
.write(toJSON(list(config = list(
workerId = .workerId,
@@ -166,6 +176,9 @@ ShinySession <- setRefClass(
},
flushOutput = function() {
.flushCallbacks$invoke()
on.exit(.flushedCallbacks$invoke())
if (length(.progressKeys) == 0
&& length(.invalidatedOutputValues) == 0
&& length(.invalidatedOutputErrors) == 0
@@ -248,6 +261,28 @@ ShinySession <- setRefClass(
# Add to input message queue
.inputMessageQueue[[length(.inputMessageQueue) + 1]] <<- data
},
onFlush = function(func, once = TRUE) {
if (!isTRUE(once)) {
return(.flushCallbacks$register(func))
} else {
dereg <- .flushCallbacks$register(function() {
dereg()
func()
})
return(dereg)
}
},
onFlushed = function(func, once = TRUE) {
if (!isTRUE(once)) {
return(.flushedCallbacks$register(func))
} else {
dereg <- .flushedCallbacks$register(function() {
dereg()
func()
})
return(dereg)
}
},
.write = function(json) {
if (getOption('shiny.trace', FALSE))
message('SEND ',
@@ -614,7 +649,7 @@ httpResponse <- function(status = 200,
return(resp)
}
httpServer <- function(handlers) {
httpServer <- function(handlers, sharedSecret) {
handler <- joinHandlers(handlers)
# TODO: Figure out what this means after httpuv migration
@@ -623,6 +658,13 @@ httpServer <- function(handlers) {
filter <- function(req, response) response
function(req) {
if (!is.null(sharedSecret)
&& !identical(sharedSecret, req$HTTP_SHINY_SHARED_SECRET)) {
return(list(status=403,
body='<h1>403 Forbidden</h1><p>Shared secret mismatch</p>',
headers=list('Content-Type' = 'text/html')))
}
response <- handler(req)
if (is.null(response))
response <- httpResponse(404, content="<h1>Not Found</h1>")
@@ -1021,6 +1063,11 @@ startApp <- function(httpHandlers, serverFuncSource, port, workerId) {
sys.www.root <- system.file('www', package='shiny')
# This value, if non-NULL, must be present on all HTTP and WebSocket
# requests as the Shiny-Shared-Secret header or else access will be
# denied (403 response for HTTP, and instant close for websocket).
sharedSecret <- getOption('shiny.sharedSecret', NULL)
httpuvCallbacks <- list(
onHeaders = function(req) {
maxSize <- getOption('shiny.maxRequestSize', 5 * 1024 * 1024)
@@ -1048,8 +1095,13 @@ startApp <- function(httpHandlers, serverFuncSource, port, workerId) {
httpHandlers,
sys.www.root,
resourcePathHandler,
reactLogHandler)),
reactLogHandler), sharedSecret),
onWSOpen = function(ws) {
if (!is.null(sharedSecret)
&& !identical(sharedSecret, ws$request$HTTP_SHINY_SHARED_SECRET)) {
ws$close()
}
shinysession <- ShinySession$new(ws, workerId)
appsByToken$set(shinysession$token, shinysession)
@@ -1136,7 +1188,28 @@ startApp <- function(httpHandlers, serverFuncSource, port, workerId) {
shinysession$dispatch(msg)
)
shinysession$manageHiddenOutputs()
flushReact()
if (exists(".shiny__stdout", globalenv()) &&
exists("HTTP_GUID", ws$request)) {
# safe to assume we're in shiny-server
shiny_stdout <- get(".shiny__stdout", globalenv())
# eNter a flushReact
writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
sep=""), con=shiny_stdout)
flush(shiny_stdout)
flushReact()
# eXit a flushReact
writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
sep=""), con=shiny_stdout)
flush(shiny_stdout)
} else {
flushReact()
}
lapply(appsByToken$values(), function(shinysession) {
shinysession$flushOutput()
NULL
@@ -1248,7 +1321,7 @@ runApp <- function(appDir=getwd(),
}
require(shiny)
if (is.character(appDir)) {
orig.wd <- getwd()
setwd(appDir)

View File

@@ -80,15 +80,53 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
pixelratio <- shinysession$clientData$pixelratio
if (is.null(pixelratio))
pixelratio <- 1
coordmap <- NULL
plotFunc <- function() {
# Actually perform the plotting
func()
outfile <- do.call(plotPNG, c(func, width=width*pixelratio,
# Now capture some graphics device info before we close it
usrCoords <- par('usr')
usrBounds <- usrCoords
if (par('xlog')) {
usrBounds[c(1,2)] <- 10 ^ usrBounds[c(1,2)]
}
if (par('ylog')) {
usrBounds[c(3,4)] <- 10 ^ usrBounds[c(3,4)]
}
coordmap <<- list(
usr = c(
left = usrCoords[1],
right = usrCoords[2],
bottom = usrCoords[3],
top = usrCoords[4]
),
# The bounds of the plot area, in DOM pixels
bounds = c(
left = grconvertX(usrBounds[1], 'user', 'nfc') * width,
right = grconvertX(usrBounds[2], 'user', 'nfc') * width,
bottom = (1-grconvertY(usrBounds[3], 'user', 'nfc')) * height,
top = (1-grconvertY(usrBounds[4], 'user', 'nfc')) * height
),
log = c(
x = par('xlog'),
y = par('ylog')
),
pixelratio = pixelratio
)
}
outfile <- do.call(plotPNG, c(plotFunc, width=width*pixelratio,
height=height*pixelratio, res=res*pixelratio, args))
on.exit(unlink(outfile))
# Return a list of attributes for the img
return(list(
src=shinysession$fileUrl(name, outfile, contentType='image/png'),
width=width, height=height))
width=width, height=height, coordmap=coordmap
))
})
}

View File

@@ -19,12 +19,20 @@ For an introduction and examples, visit the [Shiny homepage](http://www.rstudio.
## Installation
From an R console:
To install the stable version from CRAN, simply run the following from an R console:
```r
install.packages("shiny")
```
To install the latest development builds directly from GitHub, run this instead:
```r
if (!require("devtools"))
install.packages("devtools")
devtools::install_github("shiny", "rstudio")
```
## Getting Started
To learn more we highly recommend you check out the [Shiny Tutorial](http://rstudio.github.com/shiny/tutorial). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.

View File

@@ -10,7 +10,7 @@ shinyUI(pageWithSidebar(
sidebarPanel(
sliderInput("obs",
"Number of observations:",
min = 0,
min = 1,
max = 1000,
value = 500)
),

View File

@@ -663,3 +663,35 @@ test_that("Observer priorities are respected", {
expect_identical(results, c(30, 20, 21, 22, 10))
})
test_that("reactivePoll and reactiveFileReader", {
path <- tempfile('file')
on.exit(unlink(path))
write.csv(cars, file=path, row.names=FALSE)
rfr <- reactiveFileReader(100, NULL, path, read.csv)
expect_equal(isolate(rfr()), cars)
write.csv(rbind(cars, cars), file=path, row.names=FALSE)
Sys.sleep(0.15)
timerCallbacks$executeElapsed()
expect_equal(isolate(rfr()), cars)
flushReact()
expect_equal(isolate(rfr()), rbind(cars, cars))
})
test_that("classes of reactive object", {
v <- reactiveValues(a = 1)
r <- reactive({ v$a + 1 })
o <- observe({ print(r()) })
expect_false(is.reactivevalues(12))
expect_true(is.reactivevalues(v))
expect_false(is.reactivevalues(r))
expect_false(is.reactivevalues(o))
expect_false(is.reactive(12))
expect_false(is.reactive(v))
expect_true(is.reactive(r))
expect_false(is.reactive(o))
})

View File

@@ -2,27 +2,50 @@
<html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400,600' rel='stylesheet' type='text/css'>
<style type="text/css">
body {
html, body {
font-family: 'Source Sans Pro', sans-serif;
font-weight: 400;
overflow: hidden;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
div {
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
cursor: default;
}
#instructions, #ended {
position: relative;
font-weight: 200;
color: #444;
top: 20px;
font-size: 30px;
text-align: center;
}
#ended strong {
font-weight: 600;
}
svg {
border: 1px solid silver;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
width: 100%;
height: 100%;
}
.node {
cursor: pointer;
}
.node text {
font-family: 'Source Code Pro', monospace;
font-weight: normal;
text-anchor: start;
fill: #999;
transform: scale(0.3);
user-select: none;
transition: fill 0.75s ease;
}
@@ -36,7 +59,6 @@ svg {
white-space: pre;
}
.node path {
z-index: 1;
fill: white;
stroke: #777;
stroke-width: 7.5px;
@@ -55,6 +77,27 @@ svg {
.node.running path {
fill: #61B97E;
}
#legend {
font-size: 22px;
position: fixed;
bottom: 10px;
right: 20px;
}
.color {
display: inline-block;
border: 1px solid #777;
height: 14px;
width: 14px;
}
.color.normal {
background-color: #white;
}
.color.invalidated {
background-color: #E0E0E0;
}
.color.running {
background-color: #61B97E;
}
#triangle {
fill: #CCC;
}
@@ -62,7 +105,6 @@ svg {
fill: none;
stroke: #CCC;
stroke-width: 0.5px;
z-index: 0;
}
#description {
position: fixed;
@@ -70,6 +112,38 @@ svg {
left: 630px;
top: 36px;
height: auto;
display: none;
}
#timeline {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 20px;
transition: height 500ms;
}
#timeline, #timeline * {
cursor: pointer;
}
#timeline:hover {
height: 32px;
}
#timeline-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 12px;
background-color: silver;
}
#timeline-fill {
background-color: #28A3F2;
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
transition: width 500ms;
}
</style>
<script>
@@ -197,11 +271,6 @@ var force = d3.layout.force()
force.on('tick', onTick);
function pathDataForNode(node) {
/*
d="m 58,2 c -75,0 -75,100 0,100 l 60,0 l 50,-50 l -50,-50 Z"
d="m 58,2 c -75,0 -75,100 0,100 l 100,0 l 0,-100 Z"
d="m 2,0 l 0,100 l 100,0 l 50,-50 l -50,-50 Z"
*/
switch (node.type) {
case 'observer':
return 'M -25,-50 c -75,0 -75,100 0,100 l 100,0 l 0,-100 Z';
@@ -260,18 +329,20 @@ function update() {
force.size([document.documentElement.clientWidth / 4,
document.documentElement.clientHeight / 4]);
var layoutDirty = true;
var layoutDirty = false;
node = d3.select('#nodes').selectAll('.node').data(nodeList);
//layoutDirty = layoutDirty || !node.enter().empty() || !node.exit().empty();
layoutDirty = layoutDirty || !node.enter().empty() || !node.exit().empty();
var newG = node.enter().append('g')
.attr('class', function(n) {return 'node ' + n.type;})
.attr('r', 5)
// don't show until next tick
.style('display', 'none')
.on('mousedown', function() {
d3.event.stopPropagation();
})
.on('mouseover', function(n) {
$('#description').text(n.title);
$('#description').text(n.label);
})
.on('mouseout', function(d, i) {
$('#description').html('');
@@ -286,7 +357,7 @@ function update() {
newG.append('text')
.attr('x', 3)
.attr('y', 0)
.attr('font-size', 2.5)
.attr('font-size', 3.25)
.attr('transform', function(n) {
if (n.type !== 'observer')
return 'translate(1.5, 0)';
@@ -304,9 +375,15 @@ function update() {
else
return null;
});
var tspan = node.selectAll('text').selectAll('tspan')
var tspan = node.selectAll('text').filter(function(n) {
// This filter is used to disregard all nodes whose labels have
// not changed since the last time we updated them.
var changed = n.label !== this.label;
this.label = n.label;
return changed;
}).selectAll('tspan')
.data(function(n) {
var lines = n.label.split('\n');
var lines = n.label.replace(/ /g, '\xA0').split('\n');
if (lines.length > MAX_LINES) {
lines.splice(MAX_LINES);
}
@@ -323,7 +400,7 @@ function update() {
.text(function(line) { return line; });
link = d3.select('#links').selectAll('.link').data(links);
//layoutDirty = layoutDirty || !link.enter().empty() || !link.exit().empty();
layoutDirty = layoutDirty || !link.enter().empty() || !link.exit().empty();
link.enter().append('path')
.attr('class', 'link')
.attr('marker-mid', 'url(#triangle)');
@@ -339,6 +416,7 @@ function update() {
function onTick() {
node
.style('display', null)
.attr('transform', function(n) {
return 'translate(' + n.x + ' ' + n.y + ')';
});
@@ -356,7 +434,7 @@ function onTick() {
});
}
function createNode(data) {
function createNodeWithUndo(data) {
var node;
if (!data.prevId) {
node = {
@@ -365,53 +443,163 @@ function createNode(data) {
hide: data.hide
};
nodes[data.id] = node;
if (!node.hide)
pushUndo(function() {
delete nodes[data.id];
});
if (!node.hide) {
nodeList.push(node);
pushUndo(function() {
nodeList.pop();
});
}
} else {
node = nodes[data.prevId];
var oldLabel = node.label;
var oldInvalidated = node.invalidated;
delete nodes[data.prevId];
nodes[data.id] = node;
node.label = data.label;
node.invalidated = false;
pushUndo(function() {
node.label = oldLabel;
node.invalidated = oldInvalidated;
delete nodes[data.id];
nodes[data.prevId] = node;
});
}
}
Array.prototype.pushWithUndo = function(value) {
var self = this;
this.push(value);
pushUndo(function() {
self.pop();
});
}
Array.prototype.shiftWithUndo = function(value) {
var self = this;
var value = this.shift();
pushUndo(function() {
self.unshift(value);
});
return value;
}
var undoStack = [];
var currentUndos = null;
function startUndoScope() {
if (currentUndos !== null)
throw new Error('Illegal state');
currentUndos = [];
}
function pushUndo(func) {
currentUndos.push(func);
}
function endUndoScope() {
var localUndos = currentUndos;
undoStack.push(function() {
while (localUndos.length) {
localUndos.pop()();
}
});
currentUndos = null;
}
function undo() {
if (undoStack.length) {
undoStack.pop()();
update();
return true;
}
return false;
}
function undoAll() {
while (undo()) {}
}
// Here we monkeypatch Math.random to take part in the undo mechanism.
// This allows "random" d3 force-layout decisions to be reproducible.
// If we don't do this, then doing/undoing/redoing a node creation step
// looks very confusing, as the node comes flying in from a different
// direction each time.
var trueRandom = Math.random;
Math.random = (function() {
var randomStack = [];
return function() {
if (!currentUndos)
return trueRandom();
var value;
if (randomStack.length > 0) {
value = randomStack.pop();
}
else {
value = trueRandom();
}
pushUndo(function() {
randomStack.push(value);
});
return value;
};
})();
var callbacks = {
ctx: function(data) {
createNode(data);
createNodeWithUndo(data);
return true;
},
dep: function(data) {
var dependsOn = nodes[data.dependsOn];
if (!dependsOn) {
createNode({id: data.dependsOn, label: data.dependsOn, type: 'value'});
createNodeWithUndo({id: data.dependsOn, label: data.dependsOn, type: 'value'});
dependsOn = nodes[data.dependsOn];
}
if (dependsOn.hide) {
dependsOn.hide = false;
nodeList.push(dependsOn);
pushUndo(function() {
dependsOn.hide = true;
nodeList.pop();
});
}
links.push({
source: nodes[data.id],
target: nodes[data.dependsOn]
});
pushUndo(function() {
links.pop();
});
},
depId: function(data) {
links.push({
source: nodes[data.id],
target: nodes[data.dependsOn]
});
pushUndo(function() {
links.pop();
});
},
invalidate: function(data) {
var node = nodes[data.id];
if (node.invalidated)
throw new Error('Illegal sequence');
node.invalidated = true;
pushUndo(function() {
node.invalidated = false;
});
var origLinks = links;
links = links.filter(function(link) {
return link.source !== node;
});
pushUndo(function() {
links = origLinks;
});
},
valueChange: function(data) {
var existed = !!nodes[data.id];
createNode({
createNodeWithUndo({
id: data.id,
label: data.id + ' = ' + data.value,
type: 'value',
@@ -421,38 +609,94 @@ var callbacks = {
if (!existed || nodes[data.id].hide)
return true;
nodes[data.id].changed = true;
executeBeforeNextCommand.push(function() {
pushUndo(function() {
nodes[data.id].changed = false;
});
executeBeforeNextCommand.pushWithUndo(function() {
nodes[data.id].changed = false;
pushUndo(function() {
nodes[data.id].changed = true;
});
});
},
enter: function(data) {
var node = nodes[data.id];
node.running = true;
pushUndo(function() {
node.running = false;
});
},
exit: function(data) {
var node = nodes[data.id];
node.running = false;
pushUndo(function() {
node.running = true;
});
}
};
function processMessage(data) {
function processMessage(data, suppressUpdate) {
console.log(JSON.stringify(data));
if (!callbacks.hasOwnProperty(data.action))
throw new Error('Unknown action ' + data.action);
var result = callbacks[data.action].call(callbacks, data);
update();
if (!suppressUpdate)
update();
return result;
}
var executeBeforeNextCommand = [];
function doNext() {
while (executeBeforeNextCommand.length)
executeBeforeNextCommand.shift()();
while (log.length)
if (!processMessage(log.shift()))
function doNext(suppressUpdate) {
if (!log.length)
return;
startUndoScope();
while (executeBeforeNextCommand.length) {
executeBeforeNextCommand.shiftWithUndo()();
}
while (log.length) {
var result = (function() {
var message = log.shift();
pushUndo(function() {
log.unshift(message);
})
return processMessage(message, suppressUpdate);
})();
if (!result)
break;
if (log.length === 0)
document.getElementById('processNext').setAttribute('disabled', 'disabled');
}
if (!log.length) {
$('#ended').fadeIn(1500);
pushUndo(function() {
$('#ended').hide();
});
}
step++;
updateTimeline();
pushUndo(function() {
step--;
updateTimeline();
});
endUndoScope();
}
function countSteps() {
if (undoStack.length !== 0) {
throw new Error(
'Illegal state; must call countSteps before execution begins');
}
var steps = 0;
while (log.length) {
doNext();
steps++;
}
while (undoStack.length)
undoStack.pop()();
return steps;
}
function updateTimeline() {
$('#timeline-fill').width((step/totalSteps*100) + '%');
}
function zoom() {
@@ -461,18 +705,75 @@ function zoom() {
var y = d3.event.translate[1];
d3.select('#viz').attr('transform', 'scale(' + scale + ') translate(' + x/scale + ' ' + y/scale + ')');
}
// The total number of steps, as far as the user is concerned, in the log.
// This may/will be different than the number of log entries, since each
// step may include more than one log entry.
var totalSteps;
// The current step we're on.
var step;
$(function() {
d3.select('svg').call(d3.behavior.zoom().scale(4).on('zoom', zoom));
$(document.body).on('keydown', function(e) {
if (e.which === 39 || e.which === 32)
if (e.which === 39 || e.which === 32) { // space, right
// Move one step ahead
doNext();
}
if (e.which === 37) { // left
// Move one step back
undo();
}
if (e.which === 35) { // end
// Seek to end
while (log.length) {
doNext();
}
}
if (e.which === 36) { // home
// Seek to beginning
undoAll();
}
});
// Timeline click and scrub
$('#timeline').on('click mousemove', function(e) {
// Make sure left mouse button is down.
// Firefox is stupid; e.which is always 1 on mousemove events,
// even when button is not down!! So read e.originalEvent.buttons.
if (typeof(e.originalEvent.buttons) !== 'undefined') {
if (e.originalEvent.buttons !== 1)
return;
} else if (e.which !== 1) {
return;
}
var timeline = e.currentTarget;
var pos = e.offsetX || e.originalEvent.layerX;
var width = timeline.offsetWidth;
var targetStep = Math.round((pos/width) * totalSteps);
while (step < targetStep) {
doNext();
}
while (step > targetStep && step != 1) {
undo();
}
});
totalSteps = countSteps();
step = 0;
doNext();
// don't allow undoing past initial state
while (undoStack.length)
undoStack.pop();
executeBeforeNextCommand.push(function() {
$('#instructions').fadeOut(1000);
// It's weird for the instructions to fade back in, so no pushUndo here
});
});
</script>
<body>
<button id="processNext" onclick="doNext();">Next</button>
<br/>
<svg>
<defs>
<marker id="triangle"
@@ -495,6 +796,23 @@ $(function() {
<g id="nodes"></g>
</g>
</svg>
<div id="instructions">
Press right-arrow to advance
</div>
<div id="ended" style="display: none;">
<strong>You&rsquo;ve reached the end</strong><br/>Press the Home key to start over
</div>
<div id="legend">
<div class="color normal"></div> Normal<br/>
<div class="color invalidated"></div> Invalidated<br/>
<div class="color running"></div> Running<br/>
</div>
<br/>
<pre id="description"><br/></pre>
<div id="timeline">
<div id="timeline-bg">
<div id="timeline-fill"></div>
</div>
</div>
</body>
</html>

View File

@@ -82,3 +82,7 @@ span.jslider {
-o-transition: none;
transition: none;
}
.crosshair {
cursor: crosshair;
}

View File

@@ -13,6 +13,17 @@
return Math.floor(0x100000000 + (Math.random() * 0xF00000000)).toString(16);
}
// A wrapper for getComputedStyle that is compatible with older browsers.
// This is significantly faster than jQuery's .css() function.
function getStyle(el, styleProp) {
if (el.currentStyle)
var x = el.currentStyle[styleProp];
else if (window.getComputedStyle)
var x = document.defaultView.getComputedStyle(el, null)
.getPropertyValue(styleProp);
return x;
}
// Convert a number to a string with leading zeros
function padZeros(n, digits) {
var str = n.toString();
@@ -450,7 +461,7 @@
var self = this;
var createSocketFunc = exports.createSocket || function() {
var ws = new WebSocket('ws://' + window.location.host, 'shiny');
var ws = new WebSocket('ws://' + window.location.host + '/websocket/');
ws.binaryType = 'arraybuffer';
return ws;
};
@@ -991,19 +1002,106 @@
return $(scope).find('.shiny-image-output, .shiny-plot-output');
},
renderValue: function(el, data) {
var self = this;
var $el = $(el);
// Load the image before emptying, to minimize flicker
var img = null;
var coordmap, clickId, hoverId;
if (data) {
clickId = $el.data('click-id');
hoverId = $el.data('hover-id');
coordmap = data.coordmap;
delete data.coordmap;
img = document.createElement('img');
// Copy items from data to img. This should include 'src'
$.each(data, function(key, value) {
img[key] = value;
});
// Firefox doesn't have offsetX/Y, so we need to use an alternate
// method of calculation for it
function mouseOffset(mouseEvent) {
if (typeof(mouseEvent.offsetX) !== 'undefined') {
return {
x: mouseEvent.offsetX,
y: mouseEvent.offsetY
};
}
var origEvent = mouseEvent.originalEvent || {};
return {
x: origEvent.layerX - origEvent.target.offsetLeft,
y: origEvent.layerY - origEvent.target.offsetTop
};
}
function createMouseHandler(inputId) {
return function(e) {
if (e === null) {
Shiny.onInputChange(inputId, null);
return;
}
// TODO: Account for scrolling within the image??
function devToUsrX(deviceX) {
var x = deviceX - coordmap.bounds.left;
var factor = (coordmap.usr.right - coordmap.usr.left) /
(coordmap.bounds.right - coordmap.bounds.left);
return (x * factor) + coordmap.usr.left;
}
function devToUsrY(deviceY) {
var y = deviceY - coordmap.bounds.bottom;
var factor = (coordmap.usr.top - coordmap.usr.bottom) /
(coordmap.bounds.top - coordmap.bounds.bottom);
return (y * factor) + coordmap.usr.bottom;
}
var offset = mouseOffset(e);
var userX = devToUsrX(offset.x);
if (coordmap.log.x)
userX = Math.pow(10, userX);
var userY = devToUsrY(offset.y);
if (coordmap.log.y)
userY = Math.pow(10, userY);
Shiny.onInputChange(inputId, {
x: userX, y: userY,
".nonce": Math.random()
});
}
};
if (!$el.data('hover-func')) {
var hoverDelayType = $el.data('hover-delay-type') || 'debounce';
var delayFunc = (hoverDelayType === 'throttle') ? throttle : debounce;
var hoverFunc = delayFunc($el.data('hover-delay') || 300,
createMouseHandler(hoverId));
$el.data('hover-func', hoverFunc);
}
if (clickId)
$(img).on('mousedown', createMouseHandler(clickId));
if (hoverId) {
$(img).on('mousemove', $el.data('hover-func'));
$(img).on('mouseout', function(e) {
$el.data('hover-func')(null);
});
}
if (clickId || hoverId) {
$(img).addClass('crosshair');
}
}
$(el).empty();
$el.empty();
if (img)
$(el).append(img);
$el.append(img);
}
});
outputBindings.register(imageOutputBinding, 'shiny.imageOutput');
@@ -2522,7 +2620,7 @@
// non-zero, then we know that no ancestor has display:none.
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
return false;
} else if (getComputedStyle(obj, null).display === 'none') {
} else if (getStyle(obj, 'display') === 'none') {
return true;
} else {
return(isHidden(obj.parentNode));
@@ -2646,7 +2744,7 @@
});
$(document).on('keydown', function(e) {
if (e.which !== 114)
if (e.which !== 114 || (!e.ctrlKey && !e.metaKey) || (e.shiftKey || e.altKey))
return;
var url = 'reactlog?w=' + Shiny.shinyapp.config.workerId;
window.open(url);

16
man/is.reactivevalues.Rd Normal file
View File

@@ -0,0 +1,16 @@
\name{is.reactivevalues}
\alias{is.reactivevalues}
\title{Checks whether an object is a reactivevalues object}
\usage{
is.reactivevalues(x)
}
\arguments{
\item{x}{The object to test.}
}
\description{
Checks whether its argument is a reactivevalues object.
}
\seealso{
\code{\link{reactiveValues}}.
}

View File

@@ -2,7 +2,9 @@
\alias{plotOutput}
\title{Create an plot output element}
\usage{
plotOutput(outputId, width = "100\%", height = "400px")
plotOutput(outputId, width = "100\%", height = "400px",
clickId = NULL, hoverId = NULL, hoverDelay = 300,
hoverDelayType = c("debounce", "throttle"))
}
\arguments{
\item{outputId}{output variable to read the plot from}
@@ -13,6 +15,33 @@
\code{"px"} appended.}
\item{height}{Plot height}
\item{clickId}{If not \code{NULL}, the plot will send
coordinates to the server whenever it is clicked. This
information will be accessible on the \code{input} object
using \code{input$}\emph{\code{clickId}}. The value will
be a named list or vector with \code{x} and \code{y}
elements indicating the mouse position in user units.}
\item{hoverId}{If not \code{NULL}, the plot will send
coordinates to the server whenever the mouse pauses on
the plot for more than the number of milliseconds
determined by \code{hoverTimeout}. This information will
be The value will be \code{NULL} if the user is not
hovering, and a named list or vector with \code{x} and
\code{y} elements indicating the mouse position in user
units.}
\item{hoverDelay}{The delay for hovering, in
milliseconds.}
\item{hoverDelayType}{The type of algorithm for limiting
the number of hover events. Use \code{"throttle"} to
limit the number of hover events to one every
\code{hoverDelay} milliseconds. Use \code{"debounce"} to
suspend events while the cursor is moving, and wait until
the cursor has been at rest for \code{hoverDelay}
milliseconds before sending an event.}
}
\value{
A plot output element that can be included in a panel

View File

@@ -1,12 +1,16 @@
\name{reactive}
\alias{is.reactive}
\alias{reactive}
\title{Create a reactive expression}
\usage{
reactive(x, env = parent.frame(), quoted = FALSE,
label = NULL)
is.reactive(x)
}
\arguments{
\item{x}{An expression (quoted or unquoted).}
\item{x}{For \code{reactive}, an expression (quoted or
unquoted). For \code{is.reactive}, an object to test.}
\item{env}{The parent environment for the reactive
expression. By default, this is the calling environment,
@@ -21,6 +25,9 @@
\item{label}{A label for the reactive expression, useful
for debugging.}
}
\value{
a function, wrapped in a S3 class "reactive"
}
\description{
Wraps a normal expression to create a reactive
expression. Conceptually, a reactive expression is a

75
man/reactiveFileReader.Rd Normal file
View File

@@ -0,0 +1,75 @@
\name{reactiveFileReader}
\alias{reactiveFileReader}
\title{Reactive file reader}
\usage{
reactiveFileReader(intervalMillis, session, filePath,
readFunc, ...)
}
\arguments{
\item{intervalMillis}{Approximate number of milliseconds
to wait between checks of the file's last modified time.
This can be a numeric value, or a function that returns a
numeric value.}
\item{session}{The user session to associate this file
reader with, or \code{NULL} if none. If non-null, the
reader will automatically stop when the session ends.}
\item{filePath}{The file path to poll against and to pass
to \code{readFunc}. This can either be a single-element
character vector, or a function that returns one.}
\item{readFunc}{The function to use to read the file;
must expect the first argument to be the file path to
read. The return value of this function is used as the
value of the reactive file reader.}
\item{...}{Any additional arguments to pass to
\code{readFunc} whenever it is invoked.}
}
\value{
A reactive expression that returns the contents of the
file, and automatically invalidates when the file changes
on disk (as determined by last modified time).
}
\description{
Given a file path and read function, returns a reactive
data source for the contents of the file.
}
\details{
\code{reactiveFileReader} works by periodically checking
the file's last modified time; if it has changed, then
the file is re-read and any reactive dependents are
invalidated.
The \code{intervalMillis}, \code{filePath}, and
\code{readFunc} functions will each be executed in a
reactive context; therefore, they may read reactive
values and reactive expressions.
}
\examples{
\dontrun{
# Per-session reactive file reader
shinyServer(function(input, output, session)) {
fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
output$data <- renderTable({
fileData()
})
}
# Cross-session reactive file reader. In this example, all sessions share
# the same reader, so read.csv only gets executed once no matter how many
# user sessions are connected.
fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
shinyServer(function(input, output, session)) {
output$data <- renderTable({
fileData()
})
}
}
}
\seealso{
\code{\link{reactivePoll}}
}

81
man/reactivePoll.Rd Normal file
View File

@@ -0,0 +1,81 @@
\name{reactivePoll}
\alias{reactivePoll}
\title{Reactive polling}
\usage{
reactivePoll(intervalMillis, session, checkFunc,
valueFunc)
}
\arguments{
\item{intervalMillis}{Approximate number of milliseconds
to wait between calls to \code{checkFunc}. This can be
either a numeric value, or a function that returns a
numeric value.}
\item{session}{The user session to associate this file
reader with, or \code{NULL} if none. If non-null, the
reader will automatically stop when the session ends.}
\item{checkFunc}{A relatively cheap function whose values
over time will be tested for equality; inequality
indicates that the underlying value has changed and needs
to be invalidated and re-read using \code{valueFunc}. See
Details.}
\item{valueFunc}{A function that calculates the
underlying value. See Details.}
}
\value{
A reactive expression that returns the result of
\code{valueFunc}, and invalidates when \code{checkFunc}
changes.
}
\description{
Used to create a reactive data source, which works by
periodically polling a non-reactive data source.
}
\details{
\code{reactivePoll} works by pairing a relatively cheap
"check" function with a more expensive value retrieval
function. The check function will be executed
periodically and should always return a consistent value
until the data changes. When the check function returns a
different value, then the value retrieval function will
be used to re-populate the data.
Note that the check function doesn't return \code{TRUE}
or \code{FALSE} to indicate whether the underlying data
has changed. Rather, the check function indicates change
by returning a different value from the previous time it
was called.
For example, \code{reactivePoll} is used to implement
\code{reactiveFileReader} by pairing a check function
that simply returns the last modified timestamp of a
file, and a value retrieval function that actually reads
the contents of the file.
As another example, one might read a relational database
table reactively by using a check function that does
\code{SELECT MAX(timestamp) FROM table} and a value
retrieval function that does \code{SELECT * FROM table}.
The \code{intervalMillis}, \code{checkFunc}, and
\code{valueFunc} functions will be executed in a reactive
context; therefore, they may read reactive values and
reactive expressions.
}
\examples{
\dontrun{
# Assume the existence of readTimestamp and readValue functions
shinyServer(function(input, output, session) {
data <- reactivePoll(1000, session, readTimestamp, readValue)
output$dataTable <- renderTable({
data()
})
})
}
}
\seealso{
\code{\link{reactiveFileReader}}
}

View File

@@ -42,6 +42,7 @@ values <- reactiveValues(a = 1, b = 2)
isolate(values$a)
}
\seealso{
\code{\link{isolate}}.
\code{\link{isolate}} and
\code{\link{is.reactivevalues}}.
}

47
man/showReactLog.Rd Normal file
View File

@@ -0,0 +1,47 @@
\name{showReactLog}
\alias{showReactLog}
\title{Reactive Log Visualizer}
\usage{
showReactLog()
}
\description{
Provides an interactive browser-based tool for
visualizing reactive dependencies and execution in your
application.
}
\details{
To use the reactive log visualizer, start with a fresh R
session and run the command
\code{options(shiny.reactlog=TRUE)}; then launch your
application in the usual way (e.g. using
\code{\link{runApp}}). At any time you can hit Ctrl+F3
(or for Mac users, Command+F3) in your web browser to
launch the reactive log visualization.
The reactive log visualization only includes reactive
activity up until the time the report was loaded. If you
want to see more recent activity, refresh the browser.
Note that Shiny does not distinguish between reactive
dependencies that "belong" to one Shiny user session
versus another, so the visualization will include all
reactive activity that has taken place in the process,
not just for a particular application or session.
As an alternative to pressing Ctrl/Command+F3--for
example, if you are using reactives outside of the
context of a Shiny application--you can run the
\code{showReactLog} function, which will generate the
reactive log visualization as a static HTML file and
launch it in your default browser. In this case,
refreshing your browser will not load new activity into
the report; you will need to call \code{showReactLog()}
explicitly.
For security and performance reasons, do not enable
\code{shiny.reactlog} in production environments. When
the option is enabled, it's possible for any user of your
app to see at least some of the source code of your
reactive expressions and observers.
}

View File

@@ -19,9 +19,11 @@
\item{max}{The maximum value (inclusive) that can be
selected.}
\item{value}{The initial value of the slider. A warning
will be issued if the value doesn't fit between
\code{min} and \code{max}.}
\item{value}{The initial value of the slider. A numeric
vector of length one will create a regular slider; a
numeric vector of length two will create a double-ended
range slider.. A warning will be issued if the value
doesn't fit between \code{min} and \code{max}.}
\item{step}{Specifies the interval between each
selectable value on the slider (\code{NULL} means no

View File

@@ -1,6 +1,8 @@
\name{tag}
\alias{tag}
\alias{tagAppendChild}
\alias{tagAppendChildren}
\alias{tagSetChildren}
\alias{tagList}
\title{
HTML Tag Object
@@ -16,6 +18,14 @@ sets of tags; see the contents of bootstrap.R for examples.
\code{tagAppendChild(tag, child)}
\code{tagAppendChildren(tag, child1, child2)}
\code{tagAppendChildren(tag, list = list(child1, child2))}
\code{tagSetChildren(tag, child1, child2)}
\code{tagSetChildren(tag, list = list(child1, child2))}
\code{tagList(...)}
}
@@ -38,6 +48,10 @@ sets of tags; see the contents of bootstrap.R for examples.
}
\item{...}{
Unnamed items that comprise this list of tags.
}
\item{list}{
An optional list of elements. Can be used with or instead of the \code{...}
items.
}
}