Skip to content
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

feat: added a formatting function for the output of data_extract_srv #60

Merged
merged 8 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export(data_merge_module)
export(data_merge_srv)
export(datanames_input)
export(filter_spec)
export(format_data_extract)
export(get_anl_relabel_call)
export(get_dataset_prefixed_col_names)
export(get_extract_datanames)
Expand All @@ -59,4 +60,5 @@ export(variable_choices)
import(shiny)
importFrom(formatters,var_labels)
importFrom(formatters,var_relabel)
importFrom(lifecycle,badge)
importFrom(magrittr,"%>%")
1 change: 0 additions & 1 deletion R/data_extract_module.R
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ data_extract_srv <- function(id, datasets, data_extract_spec) {
return(reactive(NULL))
}
check_data_extract_spec(data_extract_spec = data_extract_spec)

res <- tryCatch(
check_data_extract_spec_react(datasets, data_extract_spec),
error = function(e) shiny::reactive(shiny::validate(e$message))
Expand Down
71 changes: 71 additions & 0 deletions R/format_data_extract.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#' @title Formatting data extracts
#' @description Returns a human-readable string representation of an extracted `data_extract_spec` object.
#'
#' @details
#' This function formats the output of [`data_extract_srv`]. See the example for more information.
#'
#' @param data_extract `list` the list output of `data_extract_srv`
#' @return `character(1)` the string representation
#' @examples
#' simple_des <- data_extract_spec(
#' dataname = "iris",
#' filter = filter_spec(vars = "Petal.Length", choices = c("1.4", "1.5")),
#' select = select_spec(choices = c("Petal.Length", "Species"))
#' )
#'
#' sample_filtered_data <- {
#' # create TealData
#' data <- teal.data::teal_data(teal.data::dataset("iris", iris))
#'
#' # covert TealData to FilteredData
#' datasets <- teal.slice:::filtered_data_new(data)
#' teal.slice:::filtered_data_set(data, datasets)
#' datasets
#' }
#'
#' if (interactive()) {
#' shiny::shinyApp(
#' ui = shiny::fluidPage(
#' data_extract_ui(
#' id = "extract",
#' label = "data extract ui",
#' data_extract_spec = simple_des,
#' is_single_dataset = TRUE
#' ),
#' shiny::verbatimTextOutput("formatted_extract")
#' ),
#' server = function(input, output, session) {
#' extracted_input <- data_extract_srv(
#' id = "extract",
#' datasets = sample_filtered_data,
#' data_extract_spec = simple_des
#' )
#' output$formatted_extract <- shiny::renderPrint({
#' format_data_extract(extracted_input())
kpagacz marked this conversation as resolved.
Show resolved Hide resolved
#' })
#' }
#' )
#' }
#' @export
#'
format_data_extract <- function(data_extract) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I could imagine this being called in a reactive context so maybe we should not have the assert and stop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then, the reactive caller should catch the error and rethrow a shiny::validate error. This function does not know about shiny.

Copy link
Contributor

Choose a reason for hiding this comment

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

True - though in practice that will never be done of course.

This function will be being called when the add_to_report button is pressed so there's unlikely to be some weird initialization issue where this function is being called maybe with DDL where data_extract is say NULL? I get very confused as to the sequence of events during load up. You could imagine adding a if(is.null(data_exrtract)) return NULL or some such thing to make people lives a little easier if needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a guard statement for null

checkmate::assert_list(data_extract)
required_names <- c("select", "filters", "dataname")
if (!checkmate::test_subset(required_names, choices = names(data_extract))) {
stop(sprintf("data_extract must be a named list with names: %s", paste0(required_names, collapse = " ")))
}

out <- sprintf("<Data Extract for dataset: %s>", data_extract$dataname)
out <- c(out, "Filters:")
for (filter in data_extract$filters) {
filtering_columns <- paste0(filter$columns, collapse = " ")
selected_values <- paste0(filter$selected, collapse = " ")
nikolas-burkoff marked this conversation as resolved.
Show resolved Hide resolved
out <- c(out, sprintf(" Columns: %s Selected: %s", filtering_columns, selected_values))
}

out <- c(out, "Selected columns:")
selected_columns <- paste0(data_extract$select, collapse = " ")
out <- c(out, sprintf(" %s", selected_columns))

paste0(out, collapse = "\n")
}
1 change: 1 addition & 0 deletions R/teal.transform-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
#' @import shiny
#' @importFrom magrittr %>%
#' @importFrom formatters var_relabel var_labels
#' @importFrom lifecycle badge
NULL
3 changes: 3 additions & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ reference:
- is_single_dataset
- list_extract_spec
- merge_datasets
- title: Human-readable formatting of a data extract object
contents:
- format_data_extract
61 changes: 61 additions & 0 deletions man/format_data_extract.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions tests/testthat/test-format_data_extract.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
required_names <- c("select", "filters", "dataname")

testthat::test_that("format_data_extract is a function that accepts a list", {
data_extract_fake <- as.list(stats::setNames(nm = required_names))
nikolas-burkoff marked this conversation as resolved.
Show resolved Hide resolved
data_extract_fake$filters <- list()
testthat::expect_error(format_data_extract(data_extract_fake), regexp = NA)
})

testthat::test_that("format_data_extract asserts its argument has required names", {
testthat::expect_error(
format_data_extract(list()),
regexp = "data_extract must be a named list with names: select filters dataname"
)
})

testthat::test_that("format_data_extract returns a string representation of the extracted data", {
data_extract_fake <- as.list(stats::setNames(nm = required_names))
data_extract_fake$dataname <- "test dataname"
data_extract_fake$filters <- list(list(columns = c("ColA", "ColB"), selected = "ColB"))
data_extract_fake$select <- c("ColC", "ColD")
data_extract_fake

testthat::expect_equal(
format_data_extract(data_extract_fake),
paste(
"<Data Extract for dataset: test dataname>",
"Filters:",
" Columns: ColA ColB Selected: ColB",
"Selected columns:",
" ColC ColD",
sep = "\n"
)
)
})

testthat::test_that("format_data_extract integrates with data_extract_srv", {
sample_filtered_data <- {
data <- teal.data::teal_data(teal.data::dataset("iris", iris))
datasets <- teal.slice:::filtered_data_new(data)
teal.slice:::filtered_data_set(data, datasets)
datasets
}

simple_des <- data_extract_spec(
dataname = "iris",
filter = filter_spec(vars = "Petal.Length", choices = c("1.4", "1.5")),
select = select_spec(choices = c("Petal.Length", "Species"))
)

shiny::testServer(
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's not ideal because data_extract_srv does not output the structure with all values filled but just a skeleton due to the use of js for some logic.

A proper test boots up a headless browser for this, which I am keen on doing.

data_extract_srv,
args = list(data_extract_spec = simple_des, datasets = sample_filtered_data),
expr = {
testthat::expect_error(format_data_extract(session$returned()), regexp = NA)
}
)
})
32 changes: 19 additions & 13 deletions vignettes/data-extract.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ knitr::opts_chunk$set(
)
```

There are times when an app developer wants to showcase more than just one fixed slice of their dataset in their
custom module. Relinquishing control of the application to a user demands the developer gives their users a degree
of freedom. In case of analyzing data, `teal` allows app developers to open up their applications to users, letting them
decide exactly what app data to analyze in the module.
There are times when an app developer wants to showcase more than just one fixed slice of their dataset in their
custom module. Relinquishing control of the application to a user demands the developer gives their users a degree
of freedom. In case of analyzing data, `teal` allows app developers to open up their applications to users, letting them
decide exactly what app data to analyze in the module.

A lot of `teal` modules use `data_extract_spec` objects and modules to tackle user input. You can find many examples in
A lot of `teal` modules use `data_extract_spec` objects and modules to tackle user input. You can find many examples in
e.g. `teal.modules.general` and `teal.modules.clinical`.


Expand All @@ -34,9 +34,9 @@ e.g. `teal.modules.general` and `teal.modules.clinical`.

#### Example module

In order to showcase different initialization options of `data_extract_spec`, first we define a `shiny` module which
uses `data_extract_ui` and `data_extract_srv` designed to handle `data_extract_spec` objects. The module creates a UI
component for single `data_extract_spec` and prints list of values returned from `data_extract_srv` module. Please see
In order to showcase different initialization options of `data_extract_spec`, first we define a `shiny` module which
uses `data_extract_ui` and `data_extract_srv` designed to handle `data_extract_spec` objects. The module creates a UI
component for single `data_extract_spec` and prints list of values returned from `data_extract_srv` module. Please see
package documentation for more information about `data_extract_ui` and `data_extract_srv`.

```{r}
Expand All @@ -54,16 +54,22 @@ extract_ui <- function(id, data_extract) {
extract_srv <- function(id, datasets, data_extract) {
moduleServer(id, function(input, output, session) {
reactive_extract_input <- data_extract_srv("data_extract", datasets, data_extract)
output$output <- renderPrint(reactive_extract_input())
s <- reactive({
str(reactive_extract_input())
reactive_extract_input()
})
output$output <- renderPrint({
s()
})
kpagacz marked this conversation as resolved.
Show resolved Hide resolved
})
}
```


#### Example data

`teal.transform` functions depend on a `FilteredData` object from the `teal.slice` package. Normally, `FilteredData` is created
automatically by `teal::init`, but for example purposes we define a wrapper function to initialize the necessary object.
`teal.transform` functions depend on a `FilteredData` object from the `teal.slice` package. Normally, `FilteredData` is created
automatically by `teal::init`, but for example purposes we define a wrapper function to initialize the necessary object.

```{r}
sample_filtered_data <- function() {
Expand All @@ -82,8 +88,8 @@ datasets <- sample_filtered_data()
```

Consider the following example, where we create two UI elements, one to filter on a specific level from `SEX` variable,
and a second one to select a variable from `c("BMRKR1", "AGE")`. `data_extract_spec` object is handed over to the shiny
app and gives instructions to generate UI components.
and a second one to select a variable from `c("BMRKR1", "AGE")`. `data_extract_spec` object is handed over to the shiny
app and gives instructions to generate UI components.

```{r}
simple_des <- data_extract_spec(
Expand Down