Open chlebowa opened 7 months ago
Being able to redefine the card function is a valid use case.
I very much agree with this. I like the request and I do like the idea for implementation - that is: a card factory function. There are some details that I am still not quite sure yet and these could potentially require changes in teal.reporter
itself (hopefully not but I cannot guarantee).
CLA Assistant Lite bot ✅ All contributors have signed the CLA
I have read the CLA Document and I hereby sign the CLA
Solves #736
Allows app developers to pass card functions to modules as arguments.
Demonstration
Example app demonstrating use of locally defined card function:
example app
``` rm(list = ls()) library(teal) devtools::load_all("./teal.modules.general") library(teal.widgets) local_card_function <- function(comment, label) { card <- teal::report_card_template( title = "Distribution Plot", label = label, with_filter = FALSE, filter_panel_api = filter_panel_api ) card$append_text("Dummy Card", "header2") card$append_text(paste( "This report card was generated by a card function", "defined in the global environment." )) if (!comment == "") { card$append_text("Comment", "header3") card$append_text(comment) } card } data <- teal_data() |> within({ iris <- iris mtcars <- mtcars faithful <- faithful }) datanames(data) <- c("iris", "mtcars", "faithful") filter <- teal_slices( teal_slice("iris", "Petal.Length", selected = c(1, 5.1)), teal_slice("iris", "Species", selected = c("setosa", "virginica")), teal_slice("mtcars", "cyl", selected = 6L), teal_slice("faithful", "waiting", selected = c(76, 82)), module_specific = TRUE, mapping = list( # "Distribution Module" = "iris Petal.Length", "Example" = "faithful waiting", "global_filters" = "iris Species" ) ) app <- init( data = data, modules = modules( tm_g_distribution( label = "Default card function", dist_var = data_extract_spec( dataname = "iris", select = select_spec(variable_choices("iris"), "Petal.Length") ), ggplot2_args = ggplot2_args( labs = list(subtitle = "Plot generated by Distribution Module") ) ), tm_g_distribution( label = "Local card function", dist_var = data_extract_spec( dataname = "iris", select = select_spec(variable_choices("iris"), "Petal.Length") ), ggplot2_args = ggplot2_args( labs = list(subtitle = "Plot generated by Distribution Module") ), card_function = local_card_function ), example_module("Example") ), filter = filter ) runApp(app, launch.browser = TRUE) ```Also works with card function defined in another package (not shown).
Rationale
Currently each module has a hard-coded card function so the module developer has full control over what is added to the report. Neither the app user, nor the app developer has any influence on this.
Being able to redefine the card function is a valid use case.
Scoping Consideration
Current card functions are defined within the module server functions because they rely heavily on bindings that exist in the execution environment of the server function. In order for a function defined elsewhere to be able to access those bindings, a function is introduced,
hydrate_function
.hydrate_function
changes the enclosing environment of a function (here, a card function) to bring another frame into scope. It can also add other bindings to the funciton's enclosure.Implementation
<module_name>_card_function
.card_function
formal argument. If not provided, falls back to the corresponding card function for the module.hydrate_function
to extend its scope to include the module server function's execution environment. This call is identical in all modules:card_function <- hydrate_function(card_function, with_filter, filter_panel_api)
.NOTE
This is a proof of concept, changes have only been made to
tm_g_distribution
.Progress