mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-12 00:19:06 -05:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f089531bd1 | ||
|
|
8d8ea53804 | ||
|
|
89e405e927 | ||
|
|
ca984a6630 | ||
|
|
fa39a55eca | ||
|
|
c3a1ba2f2d | ||
|
|
86e291f250 | ||
|
|
dd1d4439a9 | ||
|
|
cbfde18f8c | ||
|
|
e2c2e23d2a | ||
|
|
40cc5d5242 | ||
|
|
9765194ace | ||
|
|
628465e6b5 | ||
|
|
58706df120 | ||
|
|
b19225c747 | ||
|
|
c304889e61 | ||
|
|
05a9204678 | ||
|
|
1a6901c3e3 | ||
|
|
7aaba8244b | ||
|
|
8c45dcde88 | ||
|
|
6c155b04b2 | ||
|
|
cd8ad9a2ec | ||
|
|
a5db7d0246 | ||
|
|
b84b467b96 | ||
|
|
0812aaac88 | ||
|
|
194d2f911e | ||
|
|
e360b36b8a | ||
|
|
b6f66dd287 | ||
|
|
0a4bb48cd3 | ||
|
|
15d62d4a91 | ||
|
|
5b13c44ef9 | ||
|
|
0a4250f3b4 | ||
|
|
f79223ed58 | ||
|
|
2d28218a2a | ||
|
|
35974f2ee1 | ||
|
|
1f73323fb9 | ||
|
|
a3d0736eec | ||
|
|
4bdd486c00 | ||
|
|
c3895c9bd7 | ||
|
|
e9ddd89b32 | ||
|
|
88a8f2d609 | ||
|
|
a5dc5c89e8 | ||
|
|
3a15a35137 | ||
|
|
b644640804 | ||
|
|
aaa4f66671 | ||
|
|
07e021199e | ||
|
|
6b2ca7dc80 | ||
|
|
091d62803e | ||
|
|
547999bae0 | ||
|
|
d403ec7399 | ||
|
|
6ac77835df | ||
|
|
b113119a9a | ||
|
|
b713057614 | ||
|
|
d897df6a30 |
@@ -9,3 +9,4 @@
|
||||
^\.gitignore$
|
||||
^res$
|
||||
^tools$
|
||||
^man-roxygen$
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
45
NEWS
@@ -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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
34
R/graph.R
34
R/graph.R
@@ -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())
|
||||
|
||||
192
R/reactives.R
192
R/reactives.R
@@ -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()
|
||||
}
|
||||
|
||||
99
R/shiny.R
99
R/shiny.R
@@ -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)
|
||||
|
||||
@@ -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
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -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.
|
||||
|
||||
@@ -10,7 +10,7 @@ shinyUI(pageWithSidebar(
|
||||
sidebarPanel(
|
||||
sliderInput("obs",
|
||||
"Number of observations:",
|
||||
min = 0,
|
||||
min = 1,
|
||||
max = 1000,
|
||||
value = 500)
|
||||
),
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -114,6 +114,37 @@ svg {
|
||||
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>
|
||||
var log = [
|
||||
@@ -240,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';
|
||||
@@ -303,10 +329,10 @@ 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)
|
||||
@@ -331,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)';
|
||||
@@ -357,7 +383,7 @@ function update() {
|
||||
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);
|
||||
}
|
||||
@@ -374,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)');
|
||||
@@ -408,7 +434,7 @@ function onTick() {
|
||||
});
|
||||
}
|
||||
|
||||
function createNode(data) {
|
||||
function createNodeWithUndo(data) {
|
||||
var node;
|
||||
if (!data.prevId) {
|
||||
node = {
|
||||
@@ -417,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',
|
||||
@@ -473,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()))
|
||||
break;
|
||||
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) {
|
||||
$('#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() {
|
||||
@@ -513,20 +705,71 @@ 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 === 35) {
|
||||
}
|
||||
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>
|
||||
@@ -557,7 +800,7 @@ $(function() {
|
||||
Press right-arrow to advance
|
||||
</div>
|
||||
<div id="ended" style="display: none;">
|
||||
<strong>You’ve reached the end</strong><br/>Reload the page to start over
|
||||
<strong>You’ve reached the end</strong><br/>Press the Home key to start over
|
||||
</div>
|
||||
<div id="legend">
|
||||
<div class="color normal"></div> Normal<br/>
|
||||
@@ -566,5 +809,10 @@ $(function() {
|
||||
</div>
|
||||
<br/>
|
||||
<pre id="description"><br/></pre>
|
||||
<div id="timeline">
|
||||
<div id="timeline-bg">
|
||||
<div id="timeline-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -82,3 +82,7 @@ span.jslider {
|
||||
-o-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
@@ -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
16
man/is.reactivevalues.Rd
Normal 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}}.
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
75
man/reactiveFileReader.Rd
Normal 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
81
man/reactivePoll.Rd
Normal 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}}
|
||||
}
|
||||
|
||||
@@ -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
47
man/showReactLog.Rd
Normal 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.
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
14
man/tag.Rd
14
man/tag.Rd
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user