diff --git a/.Rbuildignore b/.Rbuildignore index 859608e1..c6df9eda 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,2 +1,3 @@ ^LICENSE\.md$ ^README\.(Rmd|html)$ +^\.github$ diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 00000000..2d19fc76 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..2ea1102a --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,47 @@ +on: + push: + branches: main + pull_request: + branches: main + +name: check + +jobs: + check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..d01393ce --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,49 @@ +on: + push: + branches: main + pull_request: + branches: main + +name: coverage + +jobs: + coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr + needs: coverage + + - name: Test coverage + run: | + covr::codecov( + quiet = FALSE, + clean = FALSE, + install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") + ) + shell: Rscript {0} + + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v3 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..0266f07f --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,30 @@ +on: + push: + branches: main + pull_request: + branches: main + +name: lint + +jobs: + lint: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::lintr, local::. + needs: lint + + - name: Lint + run: lintr::lint_package() + shell: Rscript {0} + env: + LINTR_ERROR_ON_LINT: true diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 100644 index 00000000..b16467e0 --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,46 @@ +on: + push: + branches: main + pull_request: + branches: main + release: + types: [published] + workflow_dispatch: + +name: pkgdown + +jobs: + pkgdown: + runs-on: ubuntu-latest + # Only restrict concurrency for non-PR jobs + concurrency: + group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Build site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy to GitHub pages 🚀 + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/NAMESPACE b/NAMESPACE index bd7e5c16..eaae10cc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,44 +5,72 @@ S3method(evalute_block,data_block) S3method(evalute_block,plot_block) S3method(evalute_block,transform_block) S3method(generate_code,block) +S3method(generate_code,transform_block) S3method(generate_server,block) +S3method(generate_server,data_block) S3method(generate_server,stack) +S3method(generate_server,transform_block) S3method(generate_ui,block) S3method(generate_ui,stack) +S3method(initialize_block,block) +S3method(initialize_block,data_block) +S3method(initialize_block,transform_block) +S3method(initialize_field,field) +S3method(is_initialized,block) +S3method(is_initialized,field) +S3method(server_code,block) +S3method(server_output,block) +S3method(ui_code,block) S3method(ui_input,field) S3method(ui_input,select_field) S3method(ui_input,string_field) +S3method(ui_output,block) S3method(ui_output,plot_block) S3method(ui_update,field) S3method(ui_update,select_field) S3method(ui_update,string_field) +S3method(update_field,field) S3method(update_fields,block) -S3method(update_fields,filter_block) +S3method(update_fields,data_block) +S3method(update_fields,transform_block) S3method(validate_field,field) S3method(validate_field,select_field) S3method(validate_field,string_field) export("%>%") +export("value<-") +export(data_block) export(evalute_block) +export(filter_block) export(generate_code) export(generate_server) export(generate_ui) +export(initialize_block) +export(initialize_field) export(is_block) export(is_field) +export(is_initialized) export(new_block) export(new_data_block) export(new_field) export(new_filter_block) export(new_plot_block) export(new_select_block) +export(new_select_field) export(new_stack) +export(new_string_field) export(select_field) export(serve_stack) -export(set_field_values_from_input) +export(server_code) +export(server_output) export(string_field) +export(ui_code) export(ui_input) export(ui_output) export(ui_update) +export(update_field) export(update_fields) export(validate_field) +export(value) +export(values) import(ggplot2) importFrom(magrittr,"%>%") diff --git a/R/block.R b/R/block.R index b6d52d62..df07cbae 100644 --- a/R/block.R +++ b/R/block.R @@ -22,7 +22,7 @@ new_block <- function(fields, expr, name = rand_names(), ..., is_string(name) ) - structure(fields, name = name, expr = expr, result = NULL, ..., + structure(fields, name = name, expr = expr, result = NULL, ..., class = c(class, "block")) } @@ -33,6 +33,35 @@ is_block <- function(x) { inherits(x, "block") } +#' @rdname new_block +#' @export +is_initialized <- function(x) { + UseMethod("is_initialized") +} + +#' @rdname new_block +#' @export +is_initialized.block <- function(x) { + all(lgl_ply(x, is_initialized)) +} + +#' @rdname new_block +#' @export +initialize_block <- function(x, ...) { + + if (is_initialized(x)) { + return(x) + } + + UseMethod("initialize_block") +} + +#' @rdname new_block +#' @export +initialize_block.block <- function(x, ...) { + stop("no base-class block initializor") +} + #' @rdname new_block #' @export generate_code <- function(x) { @@ -61,6 +90,17 @@ generate_code.block <- function(x) { do.call(bquote, list(tmp_expr, where = lapply(x, type_trans))) } +#' @rdname new_block +#' @export +generate_code.transform_block <- function(x) { + + if (!is_initialized(x)) { + return(quote(identity())) + } + + NextMethod() +} + #' @rdname new_block #' @export evalute_block <- function(x, ...) { @@ -126,12 +166,7 @@ new_data_block <- function(...) { ) expr <- quote( - get( - .(dataset), - envir = as.environment("package:datasets"), - mode = "list", - inherits = FALSE - ) + get(.(dataset), envir = as.environment("package:datasets")) ) new_block( @@ -142,26 +177,47 @@ new_data_block <- function(...) { ) } -#' @param dat Tabular data to filter (rows) -#' @param col,val Definition of the equality filter #' @rdname new_block #' @export -new_filter_block <- function(dat, col = colnames(dat)[1L], - val = NA_character_, ...) { +data_block <- function(...) { + initialize_block(new_data_block(...)) +} + +#' @rdname new_block +#' @export +initialize_block.data_block <- function(x, ...) { + + env <- list() + + for (field in names(x)) { + x[[field]] <- initialize_field(x[[field]], env) + env <- c(env, set_names(list(value(x[[field]])), field)) + } + + x +} + +#' @param data Tabular data to filter (rows) +#' @param column,value Definition of the equality filter +#' @rdname new_block +#' @export +new_filter_block <- function(data, column = character(), + value = character(), ...) { - cols <- colnames(dat) + cols <- quote(colnames(.(data))) fields <- list( - column = select_field(col, cols, type = "name"), - value = string_field(val) + column = select_field(column, cols, type = "name"), + value = string_field(value) + ) + + expr <- quote( + dplyr::filter(.(column) == .(value)) ) new_block( fields = fields, - default_expr = quote(identity()), - expr = quote( - dplyr::filter(.(column) == .(value)) - ), + expr = expr, ..., class = c("filter_block", "transform_block") ) @@ -172,7 +228,7 @@ new_filter_block <- function(dat, col = colnames(dat)[1L], #' @rdname new_block #' @export new_select_block <- function(dat, cols = colnames(dat)[1L], ...) { - all_cols <- colnames(dat) + all_cols <- quote(colnames(.(data))) # Select_field only allow one value, not multi select fields <- list( @@ -181,7 +237,6 @@ new_select_block <- function(dat, cols = colnames(dat)[1L], ...) { new_block( fields = fields, - default_expr = quote(identity()), expr = quote( dplyr::select(.(column)) ), @@ -221,34 +276,25 @@ new_plot_block <- function(dat, x, y, plot_opts = list(color = "blue"), ...) { ) ), ..., - class = c("plot_block", "plot_block") + class = c("plot_block") ) } -type_trans <- function(x) { - switch( - attr(x, "type"), - literal = c(x), - name = as.name(x) - ) -} - -set_field_value <- function(x, field, value) { - - stopifnot(inherits(x, "block")) - - value(x[[field]]) <- value - - x +#' @rdname new_block +#' @export +filter_block <- function(data, ...) { + initialize_block(new_filter_block(data, ...), data) } -set_field_values <- function(x, ...) { +#' @rdname new_block +#' @export +initialize_block.transform_block <- function(x, data, ...) { - values <- list(...) - fields <- names(values) + env <- list(data = data) - for (field in fields) { - value(x[[field]]) <- values[[field]] + for (field in names(x)) { + x[[field]] <- initialize_field(x[[field]], env) + env <- c(env, set_names(list(value(x[[field]])), field)) } x @@ -263,28 +309,47 @@ update_fields <- function(x, ...) { #' @rdname new_block #' @export update_fields.block <- function(x, ...) { - invisible(x) + stop("no base-class update fields for blocks available") } #' @param session Shiny session #' @rdname new_block #' @export -update_fields.filter_block <- function(x, data, session, ...) { - col_field <- x[["column"]] - col_choices <- colnames(data) +update_fields.data_block <- function(x, session, ...) { - if (!identical(col_choices, attr(col_field, "choices"))) { + args <- list(...) - if (!col_field %in% col_choices) { - attr(x[["column"]], "choices") <- col_choices - value(x[["column"]]) <- col_choices[1L] - value(x[["value"]]) <- NA_character_ - ui_update(x[["value"]], session, "value", "value") - } else { - meta(x[["column"]], "choices") <- col_choices - } + stopifnot(setequal(names(args), names(x))) + + for (field in names(x)) { + + env <- args[-which(names(args) == field)] + + x[[field]] <- update_field(x[[field]], args[[field]], env) + ui_update(x[[field]], session, field, field) + } + + x +} + +#' @param data Block input data +#' @rdname new_block +#' @export +update_fields.transform_block <- function(x, session, data, ...) { + + args <- list(...) + + stopifnot(setequal(names(args), names(x))) + + for (field in names(x)) { + + env <- c( + list(data = data), + args[-which(names(args) == field)] + ) - ui_update(x[["column"]], session, "column", "column") + x[[field]] <- update_field(x[[field]], args[[field]], env) + ui_update(x[[field]], session, field, field) } x diff --git a/R/field.R b/R/field.R index 451170c4..912e064f 100644 --- a/R/field.R +++ b/R/field.R @@ -4,7 +4,7 @@ #' the field holds and can be used to customize how the UI is generated. #' #' @param value Field value -#' @param ... Further (metadata) attributes +#' @param ... Further field components #' @param type Field type (allowed values are `"literal"` and `"name"`) #' @param class Field subclass #' @@ -12,14 +12,29 @@ new_field <- function(value, ..., type = c("literal", "name"), class = character()) { - structure(value, ..., type = match.arg(type), class = c(class, "field")) + x <- list(value = value, ...) + + stopifnot(is.list(x), length(unique(names(x))) == length(x)) + + structure(x, type = match.arg(type), class = c(class, "field")) +} + +#' @rdname new_block +#' @export +is_initialized.field <- function(x) { + all(lengths(values(x)) > 0) } #' @param x An object inheriting form `"field"` #' @rdname new_field #' @export validate_field <- function(x) { - UseMethod("validate_field") + + if (is_initialized(x)) { + UseMethod("validate_field", x) + } + + x } #' @rdname new_field @@ -28,6 +43,49 @@ validate_field.field <- function(x) { stop("no base-class validator for fields available") } +#' @param new Value to set +#' @param env Environment with data and other field values +#' +#' @rdname new_field +#' @export +update_field <- function(x, new, env = list()) { + UseMethod("update_field", x) +} + +#' @rdname new_field +#' @export +update_field.field <- function(x, new, env = list()) { + + x <- eval_set_field_value(x, env) + value(x) <- new + + validate_field(x) +} + +#' @rdname new_field +#' @export +initialize_field <- function(x, env = list()) { + UseMethod("initialize_field", x) +} + +#' @rdname new_field +#' @export +initialize_field.field <- function(x, env = list()) { + validate_field( + eval_set_field_value(x, env) + ) +} + +eval_set_field_value <- function(x, env) { + + for (cmp in names(x)[lgl_ply(x, is.language)]) { + expr <- do.call(bquote, list(expr = x[[cmp]], where = env)) + value(x, cmp) <- eval(expr) + } + + x +} + #' @rdname new_field #' @export is_field <- function(x) inherits(x, "field") @@ -35,53 +93,84 @@ is_field <- function(x) inherits(x, "field") #' @rdname new_field #' @export validate_field.string_field <- function(x) { - stopifnot(is_string(x)) + + val <- value(x) + + stopifnot(is.character(val), length(val) <= 1L) + x } #' @rdname new_field #' @export -string_field <- function(value, ...) { - validate_field( - new_field(value, ..., class = "string_field") - ) +new_string_field <- function(value = character(), ...) { + new_field(value, ..., class = "string_field") } +#' @rdname new_field +#' @export +string_field <- function(...) validate_field(new_string_field(...)) + #' @rdname new_field #' @export validate_field.select_field <- function(x) { - cond <- if (is.null(attr(x, "multiple"))) { - is_string(x) - } else { - length(x) > 0 && is.atomic(x) + + val <- value(x) + + stopifnot(is.character(val), length(val) <= 1L) + + if (length(val) && !val %in% value(x, "choices")) { + value(x) <- character() } - stopifnot(cond, x %in% attr(x, "choices")) + x } #' @param choices Set of permissible values #' @rdname new_field #' @export -select_field <- function(value, choices, ...) { - validate_field( - new_field(value, choices = choices, ..., class = "select_field") - ) +new_select_field <- function(value = character(), choices = character(), ...) { + new_field(value, choices = choices, ..., class = "select_field") } -`value<-` <- function(x, value) { +#' @rdname new_field +#' @export +select_field <- function(...) validate_field(new_select_field(...)) - stopifnot(inherits(x, "field")) +#' @param name Field component name +#' @rdname new_field +#' @export +value <- function(x, name = "value") { - attributes(value) <- attributes(x) + stopifnot(is_field(x)) - validate_field(value) + res <- x[[name]] + + if (is.language(res)) { + return(attr(res, "result")) + } + + res } -`meta<-` <- function(x, which, value) { +#' @rdname new_field +#' @export +values <- function(x, name = names(x)) { + set_names(lapply(name, function(n) value(x, n)), name) +} - stopifnot(inherits(x, "field")) +#' @param value Field value +#' @rdname new_field +#' @export +`value<-` <- function(x, name = "value", value) { - attr(x, which) <- value + stopifnot(is_field(x)) - validate_field(x) + if (is.language(x[[name]])) { + attr(x[[name]], "result") <- value + } else { + x[[name]] <- value + } + + x } diff --git a/R/pkg.R b/R/pkg.R index c730568e..7a89490b 100644 --- a/R/pkg.R +++ b/R/pkg.R @@ -1,3 +1,5 @@ pkg_name <- function() utils::packageName() pkg_env <- function() asNamespace(pkg_name()) + +utils::globalVariables(c(".", "..", "blk", "session")) diff --git a/R/server.R b/R/server.R index 07389fc5..30511731 100644 --- a/R/server.R +++ b/R/server.R @@ -10,39 +10,29 @@ generate_server <- function(x, ...) { UseMethod("generate_server") } - - -#' Set field values of an object from a named input list -#' -#' This function sets the fields of an object `x` using values from a named input list `input`. -#' It assumes that the field names in `x` and the names in the input list `input` match. -#' -#' @param x An object with named fields that you want to update. -#' @param input A named list containing new values for the fields in `x`. -#' -#' @return An object with updated field values. +#' @rdname generate_server #' @export -#' -#' @examples -#' \dontrun{ -#' x <- new_filter_block(dat = iris) -#' input <- list(column = "Species", value = "versicolor") -#' set_field_values_from_input(x, input) -#' } -set_field_values_from_input <- function(x, input) { - fields <- names(x) - args <- lapply(setNames(fields, fields), \(x) input[[x]]) - args$x <- x - do.call(set_field_values, args) +generate_server.block <- function(x, ...) { + stop("no base-class server for blocks available") } +#' @rdname generate_server +#' @export +generate_server.data_block <- function(x, ...) { + fields <- names(x) + quot_inp <- lapply(fields, quoted_input_entry) -#' @param in_dat Forwarded to `evalute_block()` -#' @rdname generate_server -#' @export -generate_server.block <- function(x, in_dat = NULL, ...) { + obs_expr <- splice_args( + list(..(args)), + args = quot_inp + ) + + set_expr <- splice_args( + blk(update_fields(blk(), session, ..(args))), + args = quoted_input_expression(quot_inp, fields) + ) shiny::moduleServer( attr(x, "name"), @@ -52,73 +42,66 @@ generate_server.block <- function(x, in_dat = NULL, ...) { blk <- shiny::reactiveVal(x) - data_upd_completed <- reactiveVal(FALSE) - # 1. update block by data - # This will never happen in the first block - # because no data are passed ... - if (not_null(in_dat)) { - shiny::observeEvent(in_dat(), { - message(sprintf("Update data in %s", module_name)) - inputs_updated(FALSE) - blk_upd <- update_fields(blk(), data = in_dat(), session = session) - blk(blk_upd) - data_upd_completed(TRUE) - }) - } + shiny::observeEvent( + obs_expr, + set_expr, + event.quoted = TRUE, + handler.quoted = TRUE, + ignoreInit = TRUE + ) - old_hash <- reactiveVal(isolate(hash_input(input))) - inputs_updated <- reactiveVal(FALSE) - - # 2. update block by input fields - shiny::observeEvent({ - new_hash <- hash_input(input) - input_changed <- old_hash() != new_hash - if (input_changed) { - old_hash(new_hash) - message("Allowed to update inputs") - } - if (not_null(in_dat)) { - # So that it runs once after update data - # or whenever input is changed. - # This will never run in the data module - # since no data are passed. - input_changed || req(data_upd_completed()) - } else { - req(input_changed) - } - }, { - #if ("filter_block" %in% class(x)) browser() - message(sprintf("Update inputs in %s", module_name)) - blk_upd <- set_field_values_from_input(blk(), input) - blk(blk_upd) - data_upd_completed(FALSE) - inputs_updated(TRUE) - }) - - out_dat <- NULL - if (is.null(in_dat)) { - out_dat <- shiny::reactive( - evalute_block(blk()) - ) - } else { - # For plot block, we don't need to show - # data but the output ... - if (!inherits(x, "plot_block")) { - out_dat <- shiny::reactive({ - evalute_block(blk(), data = in_dat()) - }) - output$data <- shiny::renderPrint(out_dat()) - } else { - output$plot <- renderPlot({ - evalute_block(blk(), data = in_dat()) - }) - } - } + out_dat <- shiny::reactive( + evalute_block(blk()) + ) + + output <- server_output(x, out_dat, output) + output <- server_code(x, blk, output) + + out_dat + } + ) +} + +#' @param in_dat Reactive input data +#' @rdname generate_server +#' @export +generate_server.transform_block <- function(x, in_dat, ...) { + + fields <- names(x) + + quot_inp <- lapply(fields, quoted_input_entry) + + obs_expr <- splice_args( + list(in_dat(), ..(args)), + args = quot_inp + ) - output$code <- shiny::renderPrint( - cat(deparse(generate_code(blk())), sep = "\n") + set_expr <- splice_args( + blk(update_fields(blk(), session, in_dat(), ..(args))), + args = quoted_input_expression(quot_inp, fields) + ) + + shiny::moduleServer( + attr(x, "name"), + function(input, output, session) { + + blk <- shiny::reactiveVal(x) + + shiny::observeEvent( + obs_expr, + set_expr, + event.quoted = TRUE, + handler.quoted = TRUE, + ignoreInit = TRUE ) + out_dat <- shiny::reactive( + evalute_block(blk(), data = in_dat()) + ) + + output <- server_output(x, out_dat, output) + output <- server_code(x, blk, output) + out_dat } ) @@ -146,3 +129,36 @@ generate_server.stack <- function(x, ...) { } ) } + +#' @param output Shiny output +#' @param result Block result +#' @rdname generate_ui +#' @export +server_output <- function(x, result, output) { + UseMethod("server_output", x) +} + +#' @rdname generate_ui +#' @export +server_output.block <- function(x, result, output) { + output$output <- shiny::renderPrint(result()) + output +} + +#' @param state Block state +#' @rdname generate_ui +#' @export +server_code <- function(x, state, output) { + UseMethod("server_code", x) +} + +#' @rdname generate_ui +#' @export +server_code.block <- function(x, state, output) { + + output$code <- shiny::renderPrint( + cat(deparse(generate_code(state())), sep = "\n") + ) + + output +} diff --git a/R/stack.R b/R/stack.R index 81e4a538..531b1a7d 100644 --- a/R/stack.R +++ b/R/stack.R @@ -28,7 +28,7 @@ new_stack <- function(..., name = rand_names()) { is.list(blocks), length(blocks) >= 1L, all(lgl_ply(blocks, is_block)) ) - structure(blocks, name = name, class = "stack") + structure(blocks, name = name, class = "stack") } #' @param stack An object inheriting form `"stack"` diff --git a/R/ui.R b/R/ui.R index d9bd0ca4..f1a218b2 100644 --- a/R/ui.R +++ b/R/ui.R @@ -36,31 +36,45 @@ generate_ui.block <- function(x, id, ...) { if (!inherits(x, "plot_block")) { data_switch <- bslib::input_switch( ns("data_switch"), - "Show data?" + "Show data?", + value = TRUE ) data_switch <- shiny::tagAppendAttributes( data_switch, `data-bs-toggle` = "collapse", href = sprintf("#%s", ns("collapse_data")), - `aria-expanded` = FALSE, + `aria-expanded` = "true", `aria-controls` = ns("collapse_data") ) } - div_card( - title = shiny::h4(attr(x, "name")), - bslib::layout_sidebar( - sidebar = shiny::tagList( - data_switch, - do.call(shiny::div, unname(fields)) + shiny::tagList( + # Ensure collapse is visible + shiny::tags$head( + shiny::tags$script( + sprintf("$(function() { + const bsCollapse = new bootstrap.Collapse('#%s', { + toggle: true + }); + });", + ns("collapse_data") + )) ), - shiny::verbatimTextOutput(ns("code")), - shiny::tags$div( - class = "collapse", - id = ns("collapse_data"), - custom_verbatim_output(ns("data")) - ), - plots + div_card( + title = shiny::h4(attr(x, "name")), + bslib::layout_sidebar( + sidebar = shiny::tagList( + data_switch, + do.call(shiny::div, unname(fields)) + ), + ui_code(x, ns), + shiny::tags$div( + class = "collapse", + id = ns("collapse_data"), + ui_output(x, ns) + ), + plots + ) ) ) } @@ -99,7 +113,7 @@ ui_input.field <- function(x, id, name) { #' @rdname generate_ui #' @export ui_input.string_field <- function(x, id, name) { - shiny::textInput(id, name, x) + shiny::textInput(id, name, value(x)) } #' @rdname generate_ui @@ -108,8 +122,8 @@ ui_input.select_field <- function(x, id, name) { shiny::selectInput( id, name, - attr(x, "choices"), - x, + value(x, "choices"), + value(x), # Support multi select multiple = if (!is.null(attr(x, "multiple"))) { attr(x, "multiple") @@ -147,13 +161,13 @@ ui_update.field <- function(x, session, id, name) { #' @rdname generate_ui #' @export ui_update.string_field <- function(x, session, id, name) { - shiny::updateTextInput(session, id, name, x) + shiny::updateTextInput(session, id, name, value(x)) } #' @rdname generate_ui #' @export ui_update.select_field <- function(x, session, id, name) { - shiny::updateSelectInput(session, id, name, attr(x, "choices"), x) + shiny::updateSelectInput(session, id, name, value(x, "choices"), value(x)) } #' Custom card container @@ -177,4 +191,28 @@ custom_verbatim_output <- function(id) { tmp <- shiny::verbatimTextOutput(id) tmp$attribs$style <- "max-height: 400px; overflow-y: scroll" tmp -} \ No newline at end of file +} +#' @param ns Output namespace +#' @rdname generate_ui +#' @export +ui_output <- function(x, ns) { + UseMethod("ui_output", x) +} + +#' @rdname generate_ui +#' @export +ui_output.block <- function(x, ns) { + custom_verbatim_output(ns("output")) +} + +#' @rdname generate_ui +#' @export +ui_code <- function(x, ns) { + UseMethod("ui_code", x) +} + +#' @rdname generate_ui +#' @export +ui_code.block <- function(x, ns) { + custom_verbatim_output(ns("code")) +} diff --git a/R/utils.R b/R/utils.R index 6e0f7d56..eb091f03 100644 --- a/R/utils.R +++ b/R/utils.R @@ -14,8 +14,7 @@ NULL rand_names <- function(old_names = character(0L), n = 1L, length = 15L, - chars = c(letters, LETTERS, 0L:9L), prefix = "", - suffix = "") { + chars = letters, prefix = "", suffix = "") { stopifnot( is.null(old_names) || is.character(old_names), @@ -111,7 +110,28 @@ set_names <- function(object = nm, nm) { object } -hash_input <- function(input) { - l <- names(input) - rlang::hash(lapply(l, \(x) input[[x]])) +quoted_input_entry <- function(x) { + bquote(input[[.(val)]], list(val = x)) +} + +quoted_input_expression <- function(inputs, names) { + do.call(expression, set_names(inputs, names)) +} + +splice_args <- function(expr, ...) { + do.call( + bquote, + list(expr = substitute(expr), where = list(...), splice = TRUE) + ) +} + +type_trans <- function(x) { + + res <- value(x) + + switch( + attr(x, "type"), + literal = res, + name = as.name(res) + ) } diff --git a/README.Rmd b/README.Rmd index 33f4de6c..c4bd3b90 100644 --- a/README.Rmd +++ b/README.Rmd @@ -16,6 +16,11 @@ knitr::opts_chunk$set( # blockr +[![check](https://github.com/cynkra/blockr/actions/workflows/check.yml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/check.yml) +[![coverage](https://github.com/cynkra/blockr/actions/workflows/coverage.yml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/coverage.yml) +[![pkgdown](https://github.com/cynkra/blockr/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/pkgdown.yaml) +[![lint](https://github.com/cynkra/blockr/actions/workflows/lint.yaml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/lint.yaml) +[![codecov](https://codecov.io/gh/cynkra/blockr/graph/badge.svg?token=988fQI8MPx)](https://codecov.io/gh/cynkra/blockr) Building blocks for data manipulation and visualization operations. @@ -26,7 +31,7 @@ You can install the development version of blockr from [GitHub](https://github.c ```r # install.packages("devtools") -devtools::install_github("nbenn/blockr") +devtools::install_github("cynkra/blockr") ``` ## Example @@ -37,8 +42,8 @@ A simple stack of blocks providing a dataset selector and a filter operation. library(blockr) stack <- new_stack( - new_data_block, - new_filter_block + data_block, + filter_block ) serve_stack(stack) diff --git a/README.md b/README.md index 32ee6114..cf35a6b7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ # blockr + +[![check](https://github.com/cynkra/blockr/actions/workflows/check.yml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/check.yml) +[![coverage](https://github.com/cynkra/blockr/actions/workflows/coverage.yml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/coverage.yml) +[![pkgdown](https://github.com/cynkra/blockr/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/pkgdown.yaml) +[![lint](https://github.com/cynkra/blockr/actions/workflows/lint.yaml/badge.svg)](https://github.com/cynkra/blockr/actions/workflows/lint.yaml) +[![codecov](https://codecov.io/gh/cynkra/blockr/graph/badge.svg?token=988fQI8MPx)](https://codecov.io/gh/cynkra/blockr) Building blocks for data manipulation and visualization operations. @@ -15,7 +21,7 @@ You can install the development version of blockr from ``` r # install.packages("devtools") -devtools::install_github("nbenn/blockr") +devtools::install_github("cynkra/blockr") ``` ## Example @@ -27,8 +33,8 @@ operation. library(blockr) stack <- new_stack( - new_data_block, - new_filter_block + data_block, + filter_block ) serve_stack(stack) diff --git a/man/generate_server.Rd b/man/generate_server.Rd index 9207605f..ae2b63bb 100644 --- a/man/generate_server.Rd +++ b/man/generate_server.Rd @@ -3,12 +3,18 @@ \name{generate_server} \alias{generate_server} \alias{generate_server.block} +\alias{generate_server.data_block} +\alias{generate_server.transform_block} \alias{generate_server.stack} \title{UI} \usage{ generate_server(x, ...) -\method{generate_server}{block}(x, in_dat = NULL, ...) +\method{generate_server}{block}(x, ...) + +\method{generate_server}{data_block}(x, ...) + +\method{generate_server}{transform_block}(x, in_dat, ...) \method{generate_server}{stack}(x, ...) } @@ -17,7 +23,7 @@ generate_server(x, ...) \item{...}{Generic consistency} -\item{in_dat}{Forwarded to \code{evalute_block()}} +\item{in_dat}{Reactive input data} } \description{ Generic for UI generation diff --git a/man/generate_ui.Rd b/man/generate_ui.Rd index 712da6c3..223f931c 100644 --- a/man/generate_ui.Rd +++ b/man/generate_ui.Rd @@ -1,6 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ui.R -\name{generate_ui} +% Please edit documentation in R/server.R, R/ui.R +\name{server_output} +\alias{server_output} +\alias{server_output.block} +\alias{server_code} +\alias{server_code.block} \alias{generate_ui} \alias{generate_ui.block} \alias{generate_ui.stack} @@ -14,8 +18,19 @@ \alias{ui_update.field} \alias{ui_update.string_field} \alias{ui_update.select_field} +\alias{ui_output.block} +\alias{ui_code} +\alias{ui_code.block} \title{UI} \usage{ +server_output(x, result, output) + +\method{server_output}{block}(x, result, output) + +server_code(x, state, output) + +\method{server_code}{block}(x, state, output) + generate_ui(x, ...) \method{generate_ui}{block}(x, id, ...) @@ -30,7 +45,7 @@ ui_input(x, id, name) \method{ui_input}{select_field}(x, id, name) -ui_output(x, id) +ui_output(x, ns) \method{ui_output}{plot_block}(x, id) @@ -41,16 +56,32 @@ ui_update(x, session, id, name) \method{ui_update}{string_field}(x, session, id, name) \method{ui_update}{select_field}(x, session, id, name) + +ui_output(x, ns) + +\method{ui_output}{block}(x, ns) + +ui_code(x, ns) + +\method{ui_code}{block}(x, ns) } \arguments{ \item{x}{Object for which to generate UI components} +\item{result}{Block result} + +\item{output}{Shiny output} + +\item{state}{Block state} + \item{...}{Generic consistency} \item{id}{UI IDs} \item{name}{Field name} +\item{ns}{Output namespace} + \item{session}{Shiny session} } \description{ diff --git a/man/new_block.Rd b/man/new_block.Rd index 9be53c27..4db512cd 100644 --- a/man/new_block.Rd +++ b/man/new_block.Rd @@ -1,32 +1,53 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/block.R +% Please edit documentation in R/block.R, R/field.R \name{new_block} \alias{new_block} \alias{is_block} +\alias{is_initialized} +\alias{is_initialized.block} +\alias{initialize_block} +\alias{initialize_block.block} \alias{generate_code} \alias{generate_code.block} +\alias{generate_code.transform_block} \alias{evalute_block} \alias{evalute_block.block} \alias{evalute_block.data_block} \alias{evalute_block.transform_block} \alias{evalute_block.plot_block} \alias{new_data_block} +\alias{data_block} +\alias{initialize_block.data_block} \alias{new_filter_block} \alias{new_select_block} \alias{new_plot_block} +\alias{filter_block} +\alias{initialize_block.transform_block} \alias{update_fields} \alias{update_fields.block} -\alias{update_fields.filter_block} +\alias{update_fields.data_block} +\alias{update_fields.transform_block} +\alias{is_initialized.field} \title{Blocks} \usage{ new_block(fields, expr, name = rand_names(), ..., class = character()) is_block(x) +is_initialized(x) + +\method{is_initialized}{block}(x) + +initialize_block(x, ...) + +\method{initialize_block}{block}(x, ...) + generate_code(x) \method{generate_code}{block}(x) +\method{generate_code}{transform_block}(x) + evalute_block(x, ...) \method{evalute_block}{block}(x, ...) @@ -39,17 +60,29 @@ evalute_block(x, ...) new_data_block(...) -new_filter_block(dat, col = colnames(dat)[1L], val = NA_character_, ...) +data_block(...) + +\method{initialize_block}{data_block}(x, ...) + +new_filter_block(data, column = character(), value = character(), ...) new_select_block(dat, cols = colnames(dat)[1L], ...) new_plot_block(dat, x, y, plot_opts = list(color = "blue"), ...) +filter_block(data, ...) + +\method{initialize_block}{transform_block}(x, data, ...) + update_fields(x, ...) \method{update_fields}{block}(x, ...) -\method{update_fields}{filter_block}(x, data, session, ...) +\method{update_fields}{data_block}(x, session, ...) + +\method{update_fields}{transform_block}(x, session, data, ...) + +\method{is_initialized}{field}(x) } \arguments{ \item{fields}{A list of field, each entry inheriting from \code{"field"}} @@ -66,11 +99,11 @@ of the fields)} \item{x}{X axis variable.} -\item{data}{Result from previous block} +\item{data}{Block input data} -\item{dat}{Tabular data in which to select some columns.} +\item{column, value}{Definition of the equality filter} -\item{col, val}{Definition of the equality filter} +\item{dat}{Tabular data in which to select some columns.} \item{cols}{Column(s) to select.} diff --git a/man/new_field.Rd b/man/new_field.Rd index e884f5e2..3ad32d42 100644 --- a/man/new_field.Rd +++ b/man/new_field.Rd @@ -4,11 +4,20 @@ \alias{new_field} \alias{validate_field} \alias{validate_field.field} +\alias{update_field} +\alias{update_field.field} +\alias{initialize_field} +\alias{initialize_field.field} \alias{is_field} \alias{validate_field.string_field} +\alias{new_string_field} \alias{string_field} \alias{validate_field.select_field} +\alias{new_select_field} \alias{select_field} +\alias{value} +\alias{values} +\alias{value<-} \title{Fields} \usage{ new_field(value, ..., type = c("literal", "name"), class = character()) @@ -17,20 +26,38 @@ validate_field(x) \method{validate_field}{field}(x) +update_field(x, new, env = list()) + +\method{update_field}{field}(x, new, env = list()) + +initialize_field(x, env = list()) + +\method{initialize_field}{field}(x, env = list()) + is_field(x) \method{validate_field}{string_field}(x) -string_field(value, ...) +new_string_field(value = character(), ...) + +string_field(...) \method{validate_field}{select_field}(x) -select_field(value, choices, ...) +new_select_field(value = character(), choices = character(), ...) + +select_field(...) + +value(x, name = "value") + +values(x, name = names(x)) + +value(x, name = "value") <- value } \arguments{ \item{value}{Field value} -\item{...}{Further (metadata) attributes} +\item{...}{Further field components} \item{type}{Field type (allowed values are \code{"literal"} and \code{"name"})} @@ -38,7 +65,13 @@ select_field(value, choices, ...) \item{x}{An object inheriting form \code{"field"}} +\item{new}{Value to set} + +\item{env}{Environment with data and other field values} + \item{choices}{Set of permissible values} + +\item{name}{Field component name} } \description{ Each block consists of a set of fields, which define the type of value diff --git a/man/set_field_values_from_input.Rd b/man/set_field_values_from_input.Rd deleted file mode 100644 index 4ac7b18f..00000000 --- a/man/set_field_values_from_input.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/server.R -\name{set_field_values_from_input} -\alias{set_field_values_from_input} -\title{Set field values of an object from a named input list} -\usage{ -set_field_values_from_input(x, input) -} -\arguments{ -\item{x}{An object with named fields that you want to update.} - -\item{input}{A named list containing new values for the fields in \code{x}.} -} -\value{ -An object with updated field values. -} -\description{ -This function sets the fields of an object \code{x} using values from a named input list \code{input}. -It assumes that the field names in \code{x} and the names in the input list \code{input} match. -} -\examples{ -\dontrun{ - x <- new_filter_block(dat = iris) - input <- list(column = "Species", value = "versicolor") - set_field_values_from_input(x, input) -} -} diff --git a/tests/testthat/test-block.R b/tests/testthat/test-block.R index caab8cc4..c2dd9a41 100644 --- a/tests/testthat/test-block.R +++ b/tests/testthat/test-block.R @@ -1,6 +1,6 @@ test_that("data blocks", { - block <- new_data_block() + block <- data_block() expect_s3_class(block, "data_block") expect_type(block, "list") @@ -17,14 +17,20 @@ test_that("data blocks", { test_that("filter blocks", { - block <- new_filter_block(datasets::iris) + data <- datasets::iris + + block <- filter_block(data) expect_s3_class(block, "filter_block") expect_type(block, "list") - block <- new_filter_block(datasets::iris, "Species", "setosa") + res <- evalute_block(block, data) + + expect_identical(nrow(res), nrow(data)) + + block <- filter_block(data, "Species", "setosa") - res <- evalute_block(block, datasets::iris) + res <- evalute_block(block, data) - expect_identical(nrow(res), 50L) + expect_identical(nrow(res), nrow(data[data$Species == "setosa", ])) }) diff --git a/tests/testthat/test-field.R b/tests/testthat/test-field.R index a198e7c4..ce53814d 100644 --- a/tests/testthat/test-field.R +++ b/tests/testthat/test-field.R @@ -3,7 +3,7 @@ test_that("string fields", { field <- string_field("foo") expect_s3_class(field, "string_field") - expect_type(field, "character") + expect_type(field, "list") expect_error(string_field(1)) }) @@ -13,7 +13,14 @@ test_that("select fields", { field <- select_field("a", letters) expect_s3_class(field, "select_field") - expect_type(field, "character") + expect_type(field, "list") + expect_identical(value(field), "a") - expect_error(select_field("aa", letters)) + field <- select_field("aa", letters) + + expect_s3_class(field, "select_field") + expect_type(field, "list") + expect_identical(value(field), character()) + + expect_error(select_field(1, letters)) })