Introduction to blockr for developers

David Granjon (cynkra GmbH), Karma Tarap (BMS) and John Coene (The Y Company)

Create your own blocks
supermarket

Zoom on blocks and fields 🥦 🥚

flowchart TD
  blk_data_in(Data input)
  blk_data_out[Output]
  subgraph blk_block[Block]
    subgraph blk_field1[Field 1]
      value(Value)
      title(Title)
      descr(Description)
      status(Status)
    end
    blk_field2(Field 2)
    blk_field1 --> blk_expr
    blk_field2 --> blk_expr
    blk_expr(Expression)
    blk_res(result)
    blk_expr --> blk_res
  end
  blk_data_in --> blk_block --> blk_data_out

  • Fields are ingredients.

  • A block is a recipe step:

    • (Optional) input data.
    • fields, build the block expression and translated into Shiny inputs.
    • An expression, evaluated with input data: data |> select(<COLNAMES>).
    • A result, of the evaluated expression.
    • A class for customization (see advanced part).

How does a select block look like? (1/6)


1new_select_block <- function (columns = character(), ...) {
  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

  select_expr <- quote(dplyr::select(.(columns)))

  new_block(
    fields = fields,
    expr = select_expr,
    class = c("select_block", "transform_block"),
    ...
  )
}
1
Constructor with parameters.

How does a select block look like? (2/6)


new_select_block <- function (columns = character(), ...)
{
2  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

  select_expr <- quote(dplyr::select(.(columns)))

  new_block(
    fields = fields,
    expr = select_expr,
    class = c("select_block", "transform_block"),
    ...
  )
}
2
Dynamic field (changes with data).

How does a select block look like? (3/6)


new_select_block <- function (columns = character(), ...)
{
  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

3  select_expr <- quote(dplyr::select(.(columns)))

  new_block(
    fields = fields,
    expr = select_expr,
    class = c("select_block", "transform_block"),
    ...
  )
}
3
Create the expression (to calculate the result).

How does a select block look like? (4/6)


new_select_block <- function (columns = character(), ...)
{
  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

  select_expr <- quote(dplyr::select(.(columns)))

  new_block(
    fields = fields,
    expr = select_expr,
4    class = c("select_block", "transform_block"),
    ...
  )
}
4
Add custom class transform_block.

How does a select block look like? (5/6)


new_select_block <- function (columns = character(), ...)
{
  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

  select_expr <- quote(dplyr::select(.(columns)))

  new_block(
    fields = fields,
    expr = select_expr,
    class = c("select_block", "transform_block"),
5    ...
  )
}
5
... for extra parameters like submit.

How does a select block look like? (6/6)


new_select_block <- function (columns = character(), ...)
{
  all_cols <- function(data) colnames(data)
  fields <- list(
    columns = new_select_field(columns, all_cols, multiple = TRUE, title = "Columns")
  )

  select_expr <- quote(dplyr::select(.(columns)))

6  new_block(
    fields = fields,
    expr = select_expr,
    class = c("select_block", "transform_block"),
    ...
  )
}
6
Call new_block, passing fields, an expression and custom class.

Today’s mission

Create a new cardinal1 block:

cardinal::make_table_09_gtsum

🧪 Exercise 1: create new blocks (fields)

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

}
  1. Open the Posit Cloud project.
  2. Open R/cardinal.R.
  3. Create a field for each parameter. For booleans you can use new_switch_field(<VALUE>) and new_select_field(values, choices) for other parameters. You can use the existing all_cols for dynamic choices.

🧪 Exercise 1: solution

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

1  all_cols <- function(data) colnames(data)

2  fields <- list(
    id_var = new_select_field(id_var, all_cols, title = "ID"),
    arm_var = new_select_field(arm_var, all_cols, title = "ARM"),
    saffl_var = new_select_field(saffl_var, all_cols, title = "SAFFL"),
    pref_var = new_select_field(pref_var, all_cols, title = "Pref"),
    show_colcounts = new_switch_field(show_colcounts, title = "Show column counts")
  )

  new_block(
    fields = fields,

  )
}
1
Construct columns dynamically.
2
Add field(s) for interactivity.

🧪 Exercise 2: create new blocks (expression)

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    id_var = new_select_field(id_var, all_cols, title = "ID"),
    arm_var = new_select_field(arm_var, all_cols, title = "ARM"),
    saffl_var = new_select_field(saffl_var, all_cols, title = "SAFFL"),
    pref_var = new_select_field(pref_var, all_cols, title = "Pref"),
    show_colcounts = new_switch_field(show_colcounts, title = "Show column counts")
  )

  new_block(
    fields = fields,
    expr = quote({
      cardinal::make_table_09_gtsum(
        adae = data,
        # TO DO
      )
    })
  )
}
  1. Fill in the cardinal::make_table_09_gtsum call to pass in fields name like so: <param_name> = .(field_name). This strange notation is needed by bquote for partial substitution.

🧪 Exercise 2: solution

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    id_var = new_select_field(id_var, all_cols, title = "ID"),
    arm_var = new_select_field(arm_var, all_cols, title = "ARM"),
    saffl_var = new_select_field(saffl_var, all_cols, title = "SAFFL"),
    pref_var = new_select_field(pref_var, all_cols, title = "Pref"),
    show_colcounts = new_switch_field(show_colcounts, title = "Show column counts")
  )

  new_block(
    fields = fields,
3    expr = quote({
      cardinal::make_table_09_gtsum(
        adae = data,
        arm_var = .(arm_var),
        saffl_var = .(saffl_var),
        id_var = .(id_var),
        pref_var = .(pref_var),
        show_colcounts = .(show_colcounts)
      )
    })
  )
}
3
This expression is then evaluated later with the previous block’s data and field values.

How to control block’s behavior? About blockr classes

flowchart LR
  block_generic[Block generic] --> data_block_method[Data block method]
  block_generic --> transform_block_method[Transform block method]
  block_generic --> plot_block_method[Plot block method]

S3 OO system1: customize behavior depending on object class.

uiOutputBlock.block <- function(x, ns) {
  DT::dataTableOutput(ns("res"))
}

server_output.block <- function(x, result, output) {
  DT::renderDT(
    {
      result() |>
        DT::datatable(
          selection = "none",
          options = list(
            pageLength = 5L,
            processing = FALSE
          )
        )
    },
    server = TRUE
  )
}

🧪 Exercise 3: create new blocks (adding class)

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    id_var = new_select_field(id_var, all_cols, title = "ID"),
    arm_var = new_select_field(arm_var, all_cols, title = "ARM"),
    saffl_var = new_select_field(saffl_var, all_cols, title = "SAFFL"),
    pref_var = new_select_field(pref_var, all_cols, title = "Pref"),
    show_colcounts = new_switch_field(show_colcounts, title = "Show column counts")
  )

  new_block(
    fields = fields,
    expr = quote({
      cardinal::make_table_09_gtsum(
        adae = data,
        arm_var = .(arm_var),
        saffl_var = .(saffl_var),
        id_var = .(id_var),
        pref_var = .(pref_var),
        show_colcounts = .(show_colcounts)
      )
    }),
    ...,
    class =  "<TO_REPLACE>"
  )
}
  1. Give it the correct classes. Hints:

    • This isn’t a entry point block, so no data_block class.
    • As cardinal::make_table_09_gtsum does not return data, we can’t call it transform_block.

🧪 Exercise 3: solution

new_cardinal09_block <- function(
  id_var = "USUBJID",
  arm_var = "ARM",
  saffl_var = "SAFFL",
  pref_var = "AEDECOD",
  show_colcounts = TRUE,
  ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    id_var = new_select_field(id_var, all_cols, title = "ID"),
    arm_var = new_select_field(arm_var, all_cols, title = "ARM"),
    saffl_var = new_select_field(saffl_var, all_cols, title = "SAFFL"),
    pref_var = new_select_field(pref_var, all_cols, title = "Pref"),
    show_colcounts = new_switch_field(show_colcounts, title = "Show column counts")
  )

  new_block(
    fields = fields,
    expr = quote({
      cardinal::make_table_09_gtsum(
        adae = data,
        arm_var = .(arm_var),
        saffl_var = .(saffl_var),
        id_var = .(id_var),
        pref_var = .(pref_var),
        show_colcounts = .(show_colcounts)
      )
    }),
    ...,
4    class = c("cardinal09_block")
  )
}
4
We’ll call it cardinal09_block. Note that blockr does not support any method for this class. This is important for the following.

Testing our block?

pkgload::load_all()
library(blockr)
library(cardinal)
my_stack <- new_stack(
  new_dataset_block("cadae", "random.cdisc.data"),
  new_cardinal09_block()
)
serve_stack(my_stack)
  1. Open inst/examples/app.R (code above) and run it.
  2. Why does this fail?
  3. What did we miss?

Adding missing pieces

  1. We need a function that renders gt outputs (server and ui side).
  1. We need a server method for cardinal09_block. We can inherit the transform block one.
generate_server.cardinal09_block <- blockr:::generate_server.transform_block

uiOutputBlock.cardinal09_block <- function(x, ns) {
  gt::gt_output(ns("res"))
}

server_output.cardinal09_block <- function(x, result, output) {
  gt::render_gt(result())
}

How do we make custom blocks available to users?

The registry: the blocks supermarket

register
register
Registry
unregister
Select block
Name: select block
Description: select columns in a table
Classes: select_block, tranform_block
Input: data.frame
Output: data.frame
Package: blockr
Filter block
blockr.echarts4r
New block
New block
blockr.ggplot2
New block
New block

  • Information about blocks.
  • Shared between block packages.

🧪 Exercise 4: filling the supermarket with block

  1. Still within R/cardinal.R, have a look at register_cardinal_blocks.
  2. Fill in relevant information for your new_cardinal09_block.
  3. Within R/zzz.R, call register_cardinal_blocks to register the block(s) on package load.
  4. Run the following:
pkgload::load_all()
library(blockr)
library(cardinal)
my_stack <- new_stack(
  new_dataset_block("cadae", "random.cdisc.data")
)
serve_stack(my_stack)
  1. CLick on the stack + button, look for the cardinal09_block and select it.

🧪 Exercise 4: solution

register_lm_block <- function(pkg) {
  register_block(
    constructor = new_cardinal09_block,
    name = "cardinal09 block",
    description = "Create a cardinal09 block",
    classes = c("cardinal09_block", "transform_block"),
    input = "data.frame",
    output = "gt",
    package = pkg
  )
}

# Put in zzz.R
.onLoad <- function(libname, pkgname) {
  register_lm_block(pkgname)
  invisible(NULL)
}