Skip to content

898 save app state version 4 #1048

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions R/GlobalSetter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#' Set global objects
#'
#' Class to manage global objects and bring them into scope of `shiny` modules.
#'
#' Singleton object to pass variables between modules. Instantiated in the package namespace.
#' Global objects are stored in the `session$userData` environment.
#' Every `shiny` module can register a setter functions for a global variable to be taken from storage
#' or created from scratch. Then, that function can be called anywhere in the app.
#'
#' @docType class
#' @name GlobalSetter
#' @rdname GlobalSetter
#'
#' @examples
#' GlobalSetter <- getFromNamespace("GlobalSetter", "teal")
#' setter <- GlobalSetter$new()
#' setter$add_setter("romans", function() {
#' assign("romans", .romans, envir = .GlobalEnv)
#' .romans
#' })
#' setter
#' setter$set_global("romans")
#' exists("romans", envir = .GlobalEnv, inherits = TRUE)
#'
#' @keywords internal
#'
GlobalSetter <- R6::R6Class(
classname = "GlobalSetter",

# __Public Methods ----
public = list(

#' @description
#' Set object.
#' @param obj (`character(1)`) Name of object to set.
#' @return
#' Whatever the particular setter function returns, usually the global object.
set_global = function(obj) {
if (!shiny::isRunning()) {
message("no shiny, no objects")
return(NULL)
}
checkmate::assert_string(obj)
checkmate::assert_choice(obj, private$objects)

ind <- match(obj, private$objects)
private$functions[[ind]]()
},

#' @description
#' Add setter function for global object.
#' @param obj (`character(1)`) Name of object.
#' @param fun (`function`) Setter function.
add_setter = function(obj, fun) {
checkmate::assert_string(obj)
checkmate::assert_function(fun)

ind <- match(obj, private$objects, nomatch = length(private$objects) + 1L)

if (is.element(obj, private$objects)) {
if (identical(fun, private$functions[[ind]])) {
message("this setter already exists")
return(invisible(NULL))
} else {
message(sprintf("overwriting setter function for object '%s'", obj))
}
}

private$objects[ind] <- obj
private$functions[[ind]] <- fun
invisible(NULL)
},

#' @noRd
format = function(...) {
paste(
c(
"Global Object Setter",
if (length(private$objects)) sprintf(" objects: %s", toString(private$objects)) else " empty"
),
collapse = "\n"
)
}
),

# __Private Members ----
private = list(
objects = character(0L), # objects for which setter functions have been defined
functions = list() # setter functions
),
cloneable = FALSE
)

GS <- GlobalSetter$new()
142 changes: 107 additions & 35 deletions R/module_filter_manager.R
Original file line number Diff line number Diff line change
Expand Up @@ -113,43 +113,22 @@ filter_manager_srv <- function(id, filtered_data_list, filter) {

is_module_specific <- isTRUE(attr(filter, "module_specific"))

# Register setters for global objects
GS$add_setter("slices_global", setter_slices_global)
GS$add_setter("filtered_data_list", setter_filtered_data_list)
GS$add_setter("mapping_matrix", setter_mapping_matrix)

# Create global list of slices.
# Contains all available teal_slice objects available to all modules.
# Passed whole to instances of FilteredData used for individual modules.
# Down there a subset that pertains to the data sets used in that module is applied and displayed.
slices_global <- reactiveVal(filter)

filtered_data_list <-
if (!is_module_specific) {
# Retrieve the first FilteredData from potentially nested list.
# List of length one is named "global_filters" because that name is forbidden for a module label.
list(global_filters = unlist(filtered_data_list)[[1]])
} else {
# Flatten potentially nested list of FilteredData objects while maintaining useful names.
# Simply using `unlist` would result in concatenated names.
flatten_nested <- function(x, name = NULL) {
if (inherits(x, "FilteredData")) {
setNames(list(x), name)
} else {
unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name)))
}
}
flatten_nested(filtered_data_list)
}

# Create mapping fo filters to modules in matrix form (presented as data.frame).
# Modules get NAs for filters that cannot be set for them.
mapping_matrix <- reactive({
state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id")
mapping_smooth <- lapply(filtered_data_list, function(x) {
state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id")
state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id")
states_active <- state_ids_global %in% state_ids_local
ifelse(state_ids_global %in% state_ids_allowed, states_active, NA)
})

as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE)
})
slices_global <- GS$set_global("slices_global")
# Prepare FilteredData object according to app type (global/module-specific).
filtered_data_list <- GS$set_global("filtered_data_list")
# Represent mapping of modules to filters.
mapping_matrix <- GS$set_global("mapping_matrix")
# The three objects above are also stored in the app session's userData.
# They will be used in other modules and stored when bookmarking.

output$slices_table <- renderTable(
expr = {
Expand Down Expand Up @@ -182,9 +161,9 @@ filter_manager_srv <- function(id, filtered_data_list, filter) {
})

# Call snapshot manager.
snapshot_history <- snapshot_manager_srv("snapshot_manager", slices_global, mapping_matrix, filtered_data_list)
snapshot_manager_srv("snapshot_manager")
# Call state manager.
state_manager_srv("state_manager", slices_global, mapping_matrix, filtered_data_list, snapshot_history)
state_manager_srv("state_manager")

modules_out # returned for testing purpose
})
Expand Down Expand Up @@ -252,3 +231,96 @@ filter_manager_module_srv <- function(id, module_fd, slices_global) {
slices_module # returned for testing purpose
})
}



# utility functions ----

# Functions that prepare objects.
# Objects are first scoped in sesion (userData) and if not found, created and stored.

# Obtain appropriate structure of `FilteredData` objects.
# For global app, list of length 1 named "global_filters".
# For module-specific app, list of length one-per-module, named after modules.
#' @keywords internal
#' @noRd
#'
create_filtered_data_list <- function(filtered_data_list, module_specific) {
if (!module_specific) {
# Retrieve the first FilteredData from potentially nested list.
# List of length one is named "global_filters" because that name is forbidden for a module label.
list(global_filters = unlist(filtered_data_list)[[1]])
} else {
flatten_nested(filtered_data_list)
}
}
# Flatten potentially nested list of FilteredData objects while maintaining useful names.
# Simply using `unlist` would result in concatenated names.
#' @keywords internal
#' @noRd
#'
flatten_nested <- function(x, name = NULL) {
if (inherits(x, "FilteredData")) {
setNames(list(x), name)
} else {
unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name)))
}
}

# Create mapping fo filters to modules in matrix form (presented as data.frame).
# Modules get NAs for filters that cannot be set for them.
#' @keywords internal
#' @noRd
#'
create_mapping_matrix <- function(filtered_data_list, slices_global) {
reactive({
state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id")
mapping_smooth <- lapply(filtered_data_list, function(x) {
state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id")
state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id")
states_active <- state_ids_global %in% state_ids_local
ifelse(state_ids_global %in% state_ids_allowed, states_active, NA)
})

as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE)
})
}



# setter for global variable ----
#' @keywords internal
#' @noRd
setter_slices_global <- function() {
sesh <- getDefaultReactiveDomain()
if (is.null(sesh$userData$slices_global)) {
filter <- dynGet("filter")
sesh$userData$slices_global <- reactiveVal(filter)
} else {
sesh$userData$slices_global
}
}
#' @keywords internal
#' @noRd
setter_filtered_data_list <- function() {
sesh <- getDefaultReactiveDomain()
if (is.null(sesh$userData$filtered_data_list)) {
filtered_data_list <- dynGet("filtered_data_list")
is_module_specific <- dynGet("is_module_specific")
sesh$userData$filtered_data_list <- create_filtered_data_list(filtered_data_list, is_module_specific)
} else {
sesh$userData$filtered_data_list
}
}
#' @keywords internal
#' @noRd
setter_mapping_matrix <- function() {
sesh <- getDefaultReactiveDomain()
if (is.null(sesh$userData$mapping_matrix)) {
filtered_data_list <- dynGet("filtered_data_list")
slices_global <- dynGet("slices_global")
Comment on lines +320 to +321
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seemingly independent function requires always to be called in the environment where filtered_data_list and slices_global exists (or will exist in the moment of a evaluation). This is not just unsafe but also too complicated.

Copy link
Contributor Author

@chlebowa chlebowa Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seemingly independent function

Where did you get that idea? 😆
All of this is extremely impure but If we are to share objects between modules, there is no way to do it in a pure way.

sesh$userData$mapping_matrix <- create_mapping_matrix(filtered_data_list, slices_global)
} else {
sesh$userData$mapping_matrix
}
Comment on lines +323 to +325
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existence of GlobalSetter complicates this as well. Optimally function setter_mapping_matrix should be called once and it shouldn't check whether values are set or not.

Suggested change
} else {
sesh$userData$mapping_matrix
}

}
45 changes: 31 additions & 14 deletions R/module_snapshot_manager.R
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,21 @@ snapshot_manager_ui <- function(id) {
#' @rdname snapshot_manager_module
#' @keywords internal
#'
snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) {
snapshot_manager_srv <- function(id) {
checkmate::assert_character(id)
checkmate::assert_true(is.reactive(slices_global))
checkmate::assert_class(isolate(slices_global()), "teal_slices")
checkmate::assert_true(is.reactive(mapping_matrix))
checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE)
checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named")

moduleServer(id, function(input, output, session) {
ns <- session$ns

# Retrieve global objects ----
slices_global <- GS$set_global("slices_global")
filtered_data_list <- GS$set_global("filtered_data_list")
mapping_matrix <- GS$set_global("mapping_matrix")

# Register setter for global snapshot history ----
GS$add_setter("snapshot_history", setter_snapshot_history)
# Store global filter states ----
filter <- isolate(slices_global())
snapshot_history <- reactiveVal({
list(
"Initial application state" = as.list(filter, recursive = TRUE)
)
})
snapshot_history <- GS$set_global("snapshot_history")

# Snapshot current application state ----
# Name snaphsot.
Expand Down Expand Up @@ -326,14 +323,13 @@ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_dat
}
})

return(snapshot_history)
})
}




### utility functions ----
# utility functions ----

#' Explicitly enumerate global filters.
#'
Expand All @@ -343,6 +339,7 @@ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_dat
#' @param module_names (`character`) vector containing names of all modules in the app
#' @return A `named_list` with one element per module, each element containing all filters applied to that module.
#' @keywords internal
#' @noRd
#'
unfold_mapping <- function(mapping, module_names) {
module_names <- structure(module_names, names = module_names)
Expand All @@ -360,6 +357,7 @@ unfold_mapping <- function(mapping, module_names) {
#' columns represent modules and row represent `teal_slice`s
#' @return `named list` like that in the `mapping` attribute of a `teal_slices` object.
#' @keywords internal
#' @noRd
#'
matrix_to_mapping <- function(mapping_matrix) {
mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x))
Expand All @@ -370,3 +368,22 @@ matrix_to_mapping <- function(mapping_matrix) {
mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters))
Filter(function(x) length(x) != 0L, mapping)
}



# setter for global variable ----
#' @keywords internal
#' @noRd
setter_snapshot_history <- function() {
sesh <- getDefaultReactiveDomain()
if (is.null(sesh$userData$snapshot_history)) {
slices_global <- dynGet("slices_global")
sesh$userData$snapshot_history <- reactiveVal({
list(
"Initial application state" = as.list(isolate(slices_global()), recursive = TRUE)
)
})
} else {
sesh$userData$snapshot_history
}
}
Loading