No-Code Data Analysis and Dashboards with {blockr}

John Coene (The Y Company) and David Granjon (cynkra GmbH)

A cooking approach to data science

shiny homepage

shiny homepage corrected

Developing enterprise-grade dashboards isn’t easy

💡 Introducing {blockr}

  • Supermarket for data analysis with R.
  • No-Code Dashboard builder, “Shiny’s WordPress” …
  • Extendable by developers.
  • Reproducible code.

blockr 101

Palmer penguins plot

What penguin specie has the largest flipper?

penguins |>
  filter(sex == "female") |>
  ggplot(
    aes(
      x = body_mass_g, 
      y = flipper_length_mm
    )
  ) +
  geom_point(
    aes(
      color = species, 
      shape = species
    ), 
    size = 2
  )

The stack: a data analysis recipe

Stack
data
data
Block 1
Data: dataset, browser, …
Block 2
Transform block: filter, select …
Block 3
Result/transform: plot, filter, …

Collection of instructions, blocks, from data import to wrangling/visualization.

How much code would it take with Shiny?

library(shiny)
library(bslib)
library(ggplot2)
library(palmerpenguins)

shinyApp(
  ui = page_fluid(
    layout_sidebar(
      sidebar = sidebar(
        radioButtons("sex", "Sex", unique(penguins$sex), "female"),
        selectInput(
          "xvar", 
          "X var", 
          colnames(dplyr::select(penguins, where(is.numeric))),
          "body_mass_g"
        ),
        selectInput(
          "yvar",
          "Y var",
          colnames(dplyr::select(penguins, where(is.numeric))),
          "flipper_length_mm"
        ),
        selectInput(
          "color",
          "Color and shape",
          colnames(dplyr::select(penguins, where(is.factor))),
          "species"
        )
      ),
      plotOutput("plot")
    )
  ),
  server = function(input, output, session) {
    output$plot <- renderPlot({
      penguins |>
        filter(sex == !!input$sex) |>
        ggplot(aes(x = !!input$xvar, y = !!input$yvar)) +
        geom_point(aes(color = !!input$color, shape = !!input$color), size = 2)
    })
  }
)

It’s much easier with blockr

library(blockr)
1new_stack(
2  data_block = new_dataset_block("penguins", "palmerpenguins"),
  filter_block = new_filter_block("sex", "female"),
3  plot_block = new_ggplot_block("body_mass_g", "flipper_length_mm"),
4  layer_block = new_geompoint_block("species", "species")
)
5serve_stack(stack)
1
Create the stack.
2
Import data.
3
Create the plot.
4
Add it a layer.
5
Serve a Shiny app.

Connecting stacks: towards a dinner party

The workspace

Stack 2
Stack 1
data
data
data
data
Data
Transform
Visualize
Data
Transform

Collection of recipes (stacks) to build a dashboard.

How do I create a workspace?

library(blockr)
# Creates an empty workspace
1set_workspace(
2  stack_1 = new_stack()
  stack_2 = new_stack()
)
3serve_workspace()
1
Initialise.
2
Optional: add stacks.
3
Serve Shiny app.

How far can I go with blockr?

Create your own blocks
supermarket

Today’s mission

Create a new lm() block:

lm(bill_length_mm ~ flipper_length_mm, data = penguins)

Create new blocks: lm block 1/4

1new_lm_block <- function(y = character(), predictor = character(), ...) {

}
1
Create the constructor.

Create new blocks: lm block 2/4

new_lm_block <- function(y = character(), predictor = character(), ...) {

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

3  fields <- list(
    y = new_select_field(y, all_cols, type = "name"),
    predictor = new_select_field(predictor, all_cols, type = "name")
  )

  new_block(
    fields = fields,

  )
}
2
Construct columns dynamically.
3
Add field(s): y and predictor columns (type allows to pass in cols as name instead of strings.)

Create new blocks: lm block 3/4

new_lm_block <- function(y = character(), predictor = character(), ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    y = new_select_field(y, all_cols, type = "name"),
    predictor = new_select_field(predictor, all_cols, type = "name")
  )

  new_block(
    fields = fields,
4    expr = quote({
      model <- lm(data = data, formula = .(y) ~ .(predictor))
      lm(f, data = data)
    }),

  )
}
4
Provide expression: use quote and pass field name with .(field_name). 1

Create new blocks: lm block 4/4

new_lm_block <- function(y = character(), predictor = character(), ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    y = new_select_field(y, all_cols, type = "name"),
    predictor = new_select_field(predictor, all_cols, type = "name")
  )

  new_block(
    fields = fields,
    expr = quote({
      model <- lm(data = data, formula = .(y) ~ .(predictor))
      broom::tidy(model)
    }),
    ...,
    class = c("lm_block", "transform_block")
  )
}
  1. Give it the correct classes: transform_block + a custom class.

Testing our new block

#| standalone: true
#| components: [viewer]
#| viewerHeight: 550
## file: app.R
webr::install("blockr", repos = c("https://blockr-org.github.io/webr-repos", "https://repo.r-wasm.org"))
library(blockr)
library(palmerpenguins)

new_lm_block <- function(y = character(), predictor = character(), ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    y = new_select_field(y, all_cols, type = "name"),
    predictor = new_select_field(predictor, all_cols, type = "name")
  )

  new_block(
    fields = fields,
    expr = quote({
      model <- lm(data = data, formula = .(y) ~ .(predictor))
      broom::tidy(model)
    }),
    ...,
    class = c("lm_block", "transform_block")
  )
}

stack <- new_stack(
  data_block = new_dataset_block("penguins", "palmerpenguins"), 
  lm_block = new_lm_block("bill_length_mm", "body_mass_g")
)
serve_stack(stack)

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.

Filling the supermarket with block

register_lm_block <- function(pkg) {
  register_block(
    constructor = new_lm_block,
    name = "lm block",
    description = "Create a linear model block",
    classes = c("lm_block", "transform_block"),
    input = "data.frame",
    output = "data.frame",
    package = pkg
  )
}

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

Customize blockr

  • S3 programing: allows to customize behavior depending on object class

block
name
expr
result
layout
class
+initialize_block(block)
+is_initialized(block)
+generate_code(block)
+evaluate_block(block, …)
+generate_server(block, …)
+update_fields(block, …)

We need you!

Getting started

  1. Install
pak::pak("blockr-org/blockr")

library(blockr)
serve_stack(new_stack())
  1. Enjoy!

Use blocks and build dashboards

Share dashboards with your teams to speed up data analysis

Create blocks to help your data scientists

You’re an advanced R developer, you can extend blockr!

Our team

Karma Tarap
...
Nicolas Bennett, Christoph Sax, David Granjon
...

Appendix

Zoom on blocks and fields 🥦 🥚

Block
Field 1
Value
Title
Description
Status
Field 2
Expression
result
Data input
Output

  • Fields are ingredients.
  • A block is a recipe step.

How to build a dplyr::filter block?

Filter block
depends
Select columns
Expression
Filter values
Filter function ==, !=
Result
Data input
Transformed data

data |> filter(<COLNAME> <FILTER_FUNC> <FILTER_VALUE>, ...)

# data |> filter(col1 == "test")

3 fields:

  • <COLNAME>
  • <FILTER_FUNC>
  • <FILTER_VALUE>: depends on <COLNAME>

blockr: add block demo

#| standalone: true
#| components: [viewer]
#| viewerHeight: 800
webr::install("blockr", repos = c("https://blockr-org.github.io/webr-repos", "https://repo.r-wasm.org"))
library(blockr)
library(palmerpenguins)
library(ggplot2)

new_ggplot_block <- function(col_x = character(), col_y = character(), ...) {

  data_cols <- function(data) colnames(data)

  new_block(
    fields = list(
      x = new_select_field(col_x, data_cols, type = "name"),
      y = new_select_field(col_y, data_cols, type = "name")
    ),
    expr = quote(
      ggplot(mapping = aes(x = .(x), y = .(y)))
    ),
    class = c("ggplot_block", "plot_block"),
    ...
  )
}

new_geompoint_block <- function(color = character(), shape = character(), ...) {

  data_cols <- function(data) colnames(data$data)

  new_block(
    fields = list(
      color = new_select_field(color, data_cols, type = "name"),
      shape = new_select_field(shape, data_cols, type = "name")
    ),
    expr = quote(
      geom_point(aes(color = .(color), shape = .(shape)), size = 2)
    ),
    class = c("plot_layer_block", "plot_block"),
    ...
  )
}

new_penguins_block <- function() {
  new_dataset_block("penguins", "palmerpenguins")
}

register_block(
  constructor = new_ggplot_block,
  name = "ggplot2 block",
  description = "Create a ggplot object",
  classes = c("ggplot_block", "plot_block"),
  input = "data.frame",
  output = "ggplot2",
  package = "blockr.ggplot2"
)

register_block(
  constructor = new_geompoint_block,
  name = "geompoint block",
  description = "Create a geom point plot layer",
  classes = c("plot_layer_block", "plot_block"),
  input = "ggplot2",
  output = "ggplot2",
  package = "blockr.ggplot2"
)

stack <- new_stack(
  data_block = new_dataset_block("penguins", "palmerpenguins")
  #filter_block = new_filter_block("sex", "female"), 
  #plot_block = new_ggplot_block("flipper_length_mm", "body_mass_g"),
  #layer_block = new_geompoint_block("species", "species")
)
serve_stack(stack)

blockr: workspace demo

#| standalone: true
#| components: [viewer]
#| viewerHeight: 800
webr::install("blockr", repos = c("https://blockr-org.github.io/webr-repos", "https://repo.r-wasm.org"))
library(blockr)
library(palmerpenguins)
library(ggplot2)

new_ggplot_block <- function(col_x = character(), col_y = character(), ...) {

  data_cols <- function(data) colnames(data)

  new_block(
    fields = list(
      x = new_select_field(col_x, data_cols, type = "name"),
      y = new_select_field(col_y, data_cols, type = "name")
    ),
    expr = quote(
      ggplot(mapping = aes(x = .(x), y = .(y)))
    ),
    class = c("ggplot_block", "plot_block"),
    ...
  )
}

new_geompoint_block <- function(color = character(), shape = character(), ...) {

  data_cols <- function(data) colnames(data$data)

  new_block(
    fields = list(
      color = new_select_field(color, data_cols, type = "name"),
      shape = new_select_field(shape, data_cols, type = "name")
    ),
    expr = quote(
      geom_point(aes(color = .(color), shape = .(shape)), size = 2)
    ),
    class = c("plot_layer_block", "plot_block"),
    ...
  )
}

new_penguins_block <- function() {
  new_dataset_block("penguins", "palmerpenguins")
}

new_lm_block <- function(y = character(), predictor = character(), ...) {

  all_cols <- function(data) colnames(data)

  fields <- list(
    y = new_select_field(y, all_cols, type = "name"),
    predictor = new_select_field(predictor, all_cols, type = "name")
  )

  new_block(
    fields = fields,
    expr = quote({
      model <- lm(data = data, formula = .(y) ~ .(predictor))
      broom::tidy(model)
    }),
    ...,
    class = c("lm_block", "transform_block")
  )
}

register_block(
  constructor = new_lm_block,
  name = "Linear model block",
  description = "Create a linear model",
  classes = c("lm_block", "transform_block"),
  input = "data.frame",
  output = "data.frame",
  package = "blockr.lm"
)

register_block(
  constructor = new_ggplot_block,
  name = "ggplot2 block",
  description = "Create a ggplot object",
  classes = c("ggplot_block", "plot_block"),
  input = "data.frame",
  output = "ggplot2",
  package = "blockr.ggplot2"
)

register_block(
  constructor = new_geompoint_block,
  name = "geompoint block",
  description = "Create a geom point plot layer",
  classes = c("plot_layer_block", "plot_block"),
  input = "ggplot2",
  output = "ggplot2",
  package = "blockr.ggplot2"
)

register_block(
  constructor = new_penguins_block,
  name = "penguins data block",
  description = "Create a palmer penguins dataset block",
  classes = c("dataset_block", "data_block"),
  input = NA_character_,
  output = "data.frame",
  package = "blockr.custom"
)

set_workspace(
  stack_1 = new_stack(),
  stack2 = new_stack()
)
serve_workspace(clear = FALSE)