Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 33
189 - Add Pattern Matching Callbacks for Dash R#228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
508f8f59be5ef97cb20ef8c398120f47a6cc4b6896e860e0d26372a3aa8a9fca4a196ad19ed4efd0935842192966f2539bbe2e5097ea4f5618192cd504233a1944f6fba8f58cfc58486c94455ff7723e0939dc243c30a78bcec38028e0bf44af91cb85e305277981ce2d84a88e8831ddbf63f8afb34b1184f7cdf297c0e81ace13d67166fbaa89f815d8c12ab2d9f306c01c787118828ab327c4dc5e2e3e06dd57d43a59b1bc1cfce8a4fb1c7fc09928560cd56f491a577deb4617e8730250dbce745e2fb9218b706b37d8fc45ee70b6d5431bec3c5d777ae1330eb9d5969add210d3c1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| github: plotly | ||
| custom: https://plotly.com/products/consulting-and-oem/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -7,3 +7,4 @@ node_modules/ | ||
| python/ | ||
| todo.txt | ||
| r-finance* | ||
| build/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,15 @@ | ||
| # akin to https://github.com/plotly/dash/blob/d2ebc837/dash/dependencies.py | ||
| # Helper functions for handling dependency ids or props | ||
| setWildcardId <- function(id) { | ||
| # Sort the keys of a wildcard id | ||
| id <- id[order(names(id))] | ||
| all_selectors <- vapply(id, function(x) {is.symbol(x)}, logical(1)) | ||
| id[all_selectors] <- as.character(id[all_selectors]) | ||
| id[!all_selectors] <- lapply(id[!all_selectors], function(x) {jsonlite::unbox(x)}) | ||
| return(as.character(jsonlite::toJSON(id, auto_unbox = FALSE))) | ||
| } | ||
| #' Input/Output/State definitions | ||
| #' | ||
| #' Use in conjunction with the `callback()` method from the [dash::Dash] class | ||
| @@ -8,13 +18,23 @@ | ||
| #' The `dashNoUpdate()` function permits application developers to prevent a | ||
| #' single output from updating the layout. It has no formal arguments. | ||
| #' | ||
| #' `ALL`, `ALLSMALLER` and `MATCH` are symbols corresponding to the | ||
| #' pattern-matching callback selectors with the same names. These allow you | ||
| #' to write callbacks that respond to or update an arbitrary or dynamic | ||
| #' number of components. For more information, see the `callback` section | ||
| #' in \link{dash}. | ||
| #' | ||
| #' @name dependencies | ||
| #' @param id a component id | ||
| #' @param property the component property to use | ||
| #' @rdname dependencies | ||
| #' @export | ||
| output <- function(id, property) { | ||
| if (is.list(id)) { | ||
| id = setWildcardId(id) | ||
HammadTheOne marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| structure( | ||
| dependency(id, property), | ||
| class = c("dash_dependency", "output") | ||
| @@ -24,6 +44,9 @@ output <- function(id, property) { | ||
| #' @rdname dependencies | ||
| #' @export | ||
| input <- function(id, property) { | ||
| if (is.list(id)) { | ||
| id = setWildcardId(id) | ||
| } | ||
| structure( | ||
| dependency(id, property), | ||
| class = c("dash_dependency", "input") | ||
| @@ -33,6 +56,9 @@ input <- function(id, property) { | ||
| #' @rdname dependencies | ||
| #' @export | ||
| state <- function(id, property) { | ||
| if (is.list(id)) { | ||
| id = setWildcardId(id) | ||
| } | ||
| structure( | ||
| dependency(id, property), | ||
| class = c("dash_dependency", "state") | ||
| @@ -41,6 +67,9 @@ state <- function(id, property) { | ||
| dependency <- function(id = NULL, property = NULL) { | ||
| if (is.null(id)) stop("Must specify an id", call. = FALSE) | ||
| if (is.list(id)) { | ||
| id = setWildcardId(id) | ||
| } | ||
| list( | ||
| id = id, | ||
| property = property | ||
| @@ -54,3 +83,15 @@ dashNoUpdate <- function() { | ||
| class(x) <- "no_update" | ||
| return(x) | ||
| } | ||
| #' @rdname dependencies | ||
| #' @export | ||
| ALL <- as.symbol("ALL") | ||
| #' @rdname dependencies | ||
| #' @export | ||
| ALLSMALLER <- as.symbol("ALLSMALLER") | ||
| #' @rdname dependencies | ||
| #' @export | ||
| MATCH <- as.symbol("MATCH") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -290,6 +290,22 @@ assert_no_names <- function (x) | ||
| paste(nms, collapse = "', '")), call. = FALSE) | ||
| } | ||
| assertValidWildcards <- function(dependency) { | ||
| if (is.symbol(dependency$id)) { | ||
| result <- (jsonlite::validate(as.character(dependency$id)) && grepl("{", dependency$id)) | ||
| } else { | ||
| result <- TRUE | ||
| } | ||
| if (!result) { | ||
| dependencyType <- class(dependency) | ||
| stop(sprintf("A callback %s ID contains restricted pattern matching callback selectors ALL, MATCH or ALLSMALLER. Please verify that it is formatted as a pattern matching callback list ID, or choose a different component ID.", | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still need this check now that we're using symbols for these keywords? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I can remove those now, and replace them with a different validation. | ||
| dependencyType[dependencyType %in% c("input", "output", "state")]), | ||
| call. = FALSE) | ||
| } else { | ||
| return(result) | ||
| } | ||
| } | ||
| # the following function attempts to prune remote CSS | ||
| # or local CSS/JS dependencies that either should not | ||
| # be resolved to local R package paths, or which have | ||
| @@ -403,6 +419,27 @@ assert_valid_callbacks <- function(output, params, func) { | ||
| stop(sprintf("The callback method requires that one or more properly formatted inputs are passed."), call. = FALSE) | ||
| } | ||
| # Verify that 'input', 'state' and 'output' parameters only contain 'Wildcard' keywords if they are JSON formatted ids for pattern matching callbacks | ||
| valid_wildcard_inputs <- sapply(inputs, function(x) { | ||
rpkyle marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| assertValidWildcards(x) | ||
| }) | ||
| valid_wildcard_state <- sapply(state, function(x) { | ||
| assertValidWildcards(x) | ||
| }) | ||
| if(any(sapply(output, is.list))) { | ||
| valid_wildcard_output <- sapply(output, function(x) { | ||
| assertValidWildcards(x) | ||
| }) | ||
| } else { | ||
| valid_wildcard_output <- sapply(list(output), function(x) { | ||
| assertValidWildcards(x) | ||
| }) | ||
| } | ||
| # Check that outputs are not inputs | ||
| # https://github.com/plotly/dash/issues/323 | ||
| @@ -987,29 +1024,78 @@ removeHandlers <- function(fnList) { | ||
| } | ||
| setCallbackContext <- function(callback_elements) { | ||
| states <- lapply(callback_elements$states, function(x) { | ||
| setNames(x$value, paste(x$id, x$property, sep=".")) | ||
| }) | ||
| # Set state elements for this callback | ||
| if (length(callback_elements$state[[1]]) == 0) { | ||
| states <- sapply(callback_elements$state, function(x) { | ||
| setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
| }) | ||
| } else if (is.character(callback_elements$state[[1]][[1]])) { | ||
| states <- sapply(callback_elements$state, function(x) { | ||
| setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
| }) | ||
| } else { | ||
| states <- sapply(callback_elements$state, function(x) { | ||
| states_vector <- unlist(x) | ||
| setNames(list(states_vector[grepl("value|value.", names(states_vector))]), | ||
| paste(as.character(jsonlite::toJSON(x[[1]])), x$property, sep=".")) | ||
| }) | ||
| } | ||
| splitIdProp <- function(x) unlist(strsplit(x, split = "[.]")) | ||
| triggered <- lapply(callback_elements$changedPropIds, | ||
| function(x) { | ||
| input_id <- splitIdProp(x)[1] | ||
| prop <- splitIdProp(x)[2] | ||
| id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1)) | ||
| prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1)) | ||
| value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
| list(`prop_id` = x, `value` = value) | ||
| # The following conditionals check whether the callback is a pattern-matching callback and if it has been triggered. | ||
| if (startsWith(input_id, "{")){ | ||
| id_match <- vapply(callback_elements$inputs, function(x) { | ||
| x <- unlist(x) | ||
| any(x[grepl("id.", names(x))] %in% jsonlite::fromJSON(input_id)[[1]]) | ||
| }, logical(1))[[1]] | ||
| } else { | ||
| id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1)) | ||
| } | ||
| if (startsWith(input_id, "{")){ | ||
| prop_match <- vapply(callback_elements$inputs, function(x) { | ||
| x <- unlist(x) | ||
| any(x[names(x) == "property"] %in% prop) | ||
| }, logical(1))[[1]] | ||
| } else { | ||
| prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1)) | ||
| } | ||
| if (startsWith(input_id, "{")){ | ||
| if (length(callback_elements$inputs) == 1 || !is.null(unlist(callback_elements$inputs, recursive = F)$value)) { | ||
| value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
| } else { | ||
| value <- sapply(callback_elements$inputs[id_match & prop_match][[1]], `[[`, "value") | ||
| } | ||
| } else { | ||
| value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
| } | ||
| return(list(`prop_id` = x, `value` = value)) | ||
| } | ||
| ) | ||
| inputs <- sapply(callback_elements$inputs, function(x) { | ||
| setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
| }) | ||
| ) | ||
| if (length(callback_elements$inputs[[1]]) == 0 || is.character(callback_elements$inputs[[1]][[1]])) { | ||
| inputs <- sapply(callback_elements$inputs, function(x) { | ||
| setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
| }) | ||
| } else if (length(callback_elements$inputs[[1]]) > 1) { | ||
| inputs <- sapply(callback_elements$inputs, function(x) { | ||
| inputs_vector <- unlist(x) | ||
| setNames(list(inputs_vector[grepl("value|value.", names(inputs_vector))]), paste(as.character(jsonlite::toJSON(x$id)), x$property, sep=".")) | ||
| }) | ||
| } else { | ||
| inputs <- sapply(callback_elements$inputs, function(x) { | ||
| inputs_vector <- unlist(x) | ||
| setNames(list(inputs_vector[grepl("value|value.", names(inputs_vector))]), paste(as.character(jsonlite::toJSON(x[[1]]$id)), x[[1]]$property, sep=".")) | ||
| }) | ||
| } | ||
| return(list(states=states, | ||
| triggered=unlist(triggered, recursive=FALSE), | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it looks like we're checking if
valueshas a non-zero length. I'd guess that ifvalues = NULL, then the length is zero. If this is correct, could we rewrite this as follows?