diff --git a/.Rbuildignore b/.Rbuildignore index 25a6254c..14a226b6 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -23,5 +23,6 @@ webpack.prod.js ^\.eslintrc\.js$ ^CODEOWNERS$ ^index\.md$ +^index\.Rmd$ ^wiki ^\.vscode$ diff --git a/.gitignore b/.gitignore index 2d085d04..5d2d7ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ node_modules docs inst/doc wiki + +/.quarto/ diff --git a/NAMESPACE b/NAMESPACE index 72618a76..fef0398d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -159,6 +159,7 @@ export(from_json) export(generate_code) export(generate_server) export(generate_ui) +export(get_field_name) export(get_stack_name) export(get_stack_title) export(get_workspace) diff --git a/NEWS.md b/NEWS.md index e97b7bb3..ea15b785 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,11 @@ # blockr 0.0.2.9000 -## Fixes +## Doc +- Improved `registry` and `getting started` vignettes. +- Add new `case studies` vignette to present blockr in various contexts. +- Refine GitHub readme. +## Fixes - Loading spinner is now correctly hidden when the block visual is updated. # blockr 0.0.2 diff --git a/R/field-core.R b/R/field-core.R index fc28b27c..6230b920 100644 --- a/R/field-core.R +++ b/R/field-core.R @@ -280,6 +280,10 @@ get_sub_fields <- function(x) get_field_value(x, "sub_fields") set_sub_fields <- function(x, val) set_field_value(x, val, "sub_fields") +#' @export +#' @rdname update_field +#' @param field Field element. +#' @param name To pass a name if no title attribute. get_field_name <- function(field, name = "") { title <- attr(field, "title") diff --git a/README.Rmd b/README.Rmd index d461d73d..62cb16ae 100644 --- a/README.Rmd +++ b/README.Rmd @@ -9,12 +9,12 @@ always_allow_html: true knitr::opts_chunk$set( collapse = TRUE, comment = "#>", - fig.path = "man/figures/README-", - out.width = "100%" + fig.path = "man/figures/README-" ) library(blockr) library(bslib) +library(DiagrammeR) ``` # blockr @@ -24,31 +24,124 @@ library(bslib) [![codecov](https://codecov.io/github/blockr-org/blockr/graph/badge.svg?token=9AO88LK8FJ)](https://codecov.io/github/blockr-org/blockr) -Building blocks for data manipulation and visualization operations. +> {blockr} is Shiny's WordPress (John Coene, 2024) + +## Why blockr? + +`{blockr}` is an R package designed to democratize data analysis by providing a flexible, intuitive, and __code-free__ approach to building data pipelines. It allows users to create __powerful__ data workflows using pre-built __blocks__ that can be easily __connected__, all without writing a single line of code. + +```{r, echo=FALSE, eval=TRUE, message=FALSE} +mermaid(" + flowchart TD + subgraph LR workspace[Workspace] + subgraph stack1[Stack] + direction LR + subgraph input_block[Block 1] + input(Data: dataset, browser, ...) + end + subgraph transform_block[Block 2] + transform(Transform block: filter, select ...) + end + subgraph output_block[Block 3] + output(Result/transform: plot, filter, ...) + end + input_block --> |data| transform_block --> |data| output_block + end + subgraph stack2[Stack 2] + stack1_data[Stack 1 data] --> |data| transform2[Transform] + end + stack1 --> |data| stack2 + subgraph stackn[Stack n] + stacki_data[Stack i data] --> |data| transformn[Transform] --> |data| Visualize + end + stack2 ---> |... data| stackn + end + ", + width = "100%" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` -`{blockr}` has been built for webR (wasm) and is available for download with -`webr::install("blockr", repos = c("https://blockr-org.github.io/webr-repos", "https://repo.r-wasm.org"))`. +To get started, we invite you to read this [vignette](https://blockr-org.github.io/blockr/articles/blockr.html). -```r -library(blockr) -library(blockr.data) +To get a better idea of `{blockr}` capabilities in various data context, you can look at this [vignette](https://blockr-org.github.io/blockr/articles/blockr_examples.html). -data_block <- new_dataset_block(selected = "lab", package = "blockr.data") +## Key features -stack <- new_stack( - data_block, - new_select_block -) -serve_stack(stack) -``` +1. **User-Friendly Interface**: Build data pipelines with intuitive interface. +2. **Flexibility**: Easily add, remove, or rearrange blocks in your pipeline. +3. **Extensibility**: Developers can create custom blocks to extend functionality. +4. **Reproducibility**: Pipelines created with `blockr` are easily shareable and reproducible, with exportable code. +5. **Interactivity**: Real-time feedback as you build and modify your pipeline. ## Installation You can install the development version of blockr from [GitHub](https://github.com/) with: ```r -# install.packages("devtools") -pak::pkg_install("blockr-org/blockr") +pak::pak("blockr-org/blockr") +``` + +## Example: palmer penguins case study + +Below is a simple case study involving `{blockr}`. We use the palmerpenguins dataset to find out which +femal species has the largest flippers. We create 2 custom blocks allowing to create our plot block (see the plot vignette for more details). Note that the `{blockr.ggplot2}` package exposes some ready to use blocks. + +```{r, eval=FALSE} +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"), + ... + ) +} + +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) +``` + +```{r blockr-penguins-stack, echo=FALSE, fig.cap='Penguins app demo', fig.align = 'center', out.width='100%'} +knitr::include_graphics("vignettes/figures/blockr-penguins-stack.png") ``` ## Contribute diff --git a/README.md b/README.md index 35a866b2..71c6ba7c 100644 --- a/README.md +++ b/README.md @@ -9,34 +9,111 @@ [![codecov](https://codecov.io/github/blockr-org/blockr/graph/badge.svg?token=9AO88LK8FJ)](https://codecov.io/github/blockr-org/blockr) -Building blocks for data manipulation and visualization operations. +> {blockr} is Shiny’s WordPress (John Coene, 2024) -`{blockr}` has been built for webR (wasm) and is available for download -with -`webr::install("blockr", repos = c("https://blockr-org.github.io/webr-repos", "https://repo.r-wasm.org"))`. +## Why blockr? + +`{blockr}` is an R package designed to democratize data analysis by +providing a flexible, intuitive, and **code-free** approach to building +data pipelines. It allows users to create **powerful** data workflows +using pre-built **blocks** that can be easily **connected**, all without +writing a single line of code. + +![](man/figures/README-unnamed-chunk-2-1.png) + +To get started, we invite you to read this +[vignette](https://blockr-org.github.io/blockr/articles/blockr.html). + +To get a better idea of `{blockr}` capabilities in various data context, +you can look at this +[vignette](https://blockr-org.github.io/blockr/articles/blockr_examples.html). + +## Key features + +1. **User-Friendly Interface**: Build data pipelines with intuitive + interface. +2. **Flexibility**: Easily add, remove, or rearrange blocks in your + pipeline. +3. **Extensibility**: Developers can create custom blocks to extend + functionality. +4. **Reproducibility**: Pipelines created with `blockr` are easily + shareable and reproducible, with exportable code. +5. **Interactivity**: Real-time feedback as you build and modify your + pipeline. + +## Installation + +You can install the development version of blockr from +[GitHub](https://github.com/) with: + +``` r +pak::pak("blockr-org/blockr") +``` + +## Example: palmer penguins case study + +Below is a simple case study involving `{blockr}`. We use the +palmerpenguins dataset to find out which femal species has the largest +flippers. We create 2 custom blocks allowing to create our plot block +(see the plot vignette for more details). Note that the +`{blockr.ggplot2}` package exposes some ready to use blocks. ``` r library(blockr) -library(blockr.data) +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_block <- new_dataset_block(selected = "lab", package = "blockr.data") + 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"), + ... + ) +} stack <- new_stack( - data_block, - new_select_block + 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) ``` -## Installation +
-You can install the development version of blockr from -[GitHub](https://github.com/) with: +Penguins app demo +

+Penguins app demo +

-``` r -# install.packages("devtools") -pak::pkg_install("blockr-org/blockr") -``` +
## Contribute diff --git a/_pkgdown.yml b/_pkgdown.yml index 87194d2a..43dc59d2 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -13,7 +13,7 @@ navbar: bg: info type: light structure: - right: [reference, news, github, dark-mode] + right: [reference, github, dark-mode] components: home: ~ dark-mode: @@ -24,9 +24,11 @@ articles: desc: ~ navbar: User Guide contents: + - "`blockr_examples`" - "`data-blocks`" - "`registry`" - "`plot-block`" + - "`new-field`" - "`developer-guide`" reference: diff --git a/index.Rmd b/index.Rmd new file mode 100644 index 00000000..6444deb2 --- /dev/null +++ b/index.Rmd @@ -0,0 +1,161 @@ +--- +output: github_document +always_allow_html: true +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.path = "man/figures/README-" +) + +library(blockr) +library(bslib) +library(DiagrammeR) +``` + + + + +# blockr + + + +[![ci](https://github.com/blockr-org/blockr/actions/workflows/ci.yml/badge.svg)](https://github.com/blockr-org/blockr/actions/workflows/ci.yml) +[![codecov](https://codecov.io/github/blockr-org/blockr/graph/badge.svg?token=9AO88LK8FJ)](https://codecov.io/github/blockr-org/blockr) + + +> {blockr} is Shiny's WordPress (John Coene, 2024) + +## Why blockr? + +`{blockr}` is an R package designed to democratize data analysis by providing a flexible, intuitive, and __code-free__ approach to building data pipelines. It allows users to create __powerful__ data workflows using pre-built __blocks__ that can be easily __connected__, all without writing a single line of code. + +```{r, echo=FALSE, message=FALSE} +mermaid(" + flowchart TD + subgraph LR workspace[Workspace] + subgraph stack1[Stack] + direction LR + subgraph input_block[Block 1] + input(Data: dataset, browser, ...) + end + subgraph transform_block[Block 2] + transform(Transform block: filter, select ...) + end + subgraph output_block[Block 3] + output(Result/transform: plot, filter, ...) + end + input_block --> |data| transform_block --> |data| output_block + end + subgraph stack2[Stack 2] + stack1_data[Stack 1 data] --> |data| transform2[Transform] + end + stack1 --> |data| stack2 + subgraph stackn[Stack n] + stacki_data[Stack i data] --> |data| transformn[Transform] --> |data| Visualize + end + stack2 ---> |... data| stackn + end + ", + width = "100%" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` + +To get started, we invite you to read this [vignette](https://blockr-org.github.io/blockr/articles/blockr.html). + +To get a better idea of `{blockr}` capabilities in various data context, you can look at this [vignette](https://blockr-org.github.io/blockr/articles/blockr_examples.html). + +## Key features + +1. **User-Friendly Interface**: Build data pipelines with intuitive interface. +2. **Flexibility**: Easily add, remove, or rearrange blocks in your pipeline. +3. **Extensibility**: Developers can create custom blocks to extend functionality. +4. **Reproducibility**: Pipelines created with `blockr` are easily shareable and reproducible, with exportable code. +5. **Interactivity**: Real-time feedback as you build and modify your pipeline. + +## Installation + +You can install the development version of blockr from [GitHub](https://github.com/) with: + +```r +pak::pak("blockr-org/blockr") +``` + +## Example: palmer penguins case study + +Below is a simple case study involving `{blockr}`. We use the palmerpenguins dataset to find out which +femal species has the largest flippers. We create 2 custom blocks allowing to create our plot block (see the plot vignette for more details). Note that the `{blockr.ggplot2}` package exposes some ready to use blocks. + +
+
+ +
+ + + + + +
+ +```{r, eval=FALSE} +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"), + ... + ) +} + +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) +``` + +## Contribute + +Easiest is to run `make`, otherwise: + +1. Install npm dependencies with `packer::npm_install()` +2. Build CSS by running the script in `dev/sass.R` diff --git a/index.md b/index.md index 3fd0c8cd..688c4d74 100644 --- a/index.md +++ b/index.md @@ -1,5 +1,5 @@ - + # blockr @@ -9,32 +9,37 @@ [![codecov](https://codecov.io/github/blockr-org/blockr/graph/badge.svg?token=9AO88LK8FJ)](https://codecov.io/github/blockr-org/blockr) -Building blocks for data manipulation and visualization operations. +> {blockr} is Shiny’s WordPress (John Coene, 2024) -
-
- -
- - - - - -
+## Why blockr? -`{blockr}` has been built for webR (wasm) and is available for download -with -`webr::install("blockr", repos = "https://blockr-org.github.io/webr-repos")`. +`{blockr}` is an R package designed to democratize data analysis by +providing a flexible, intuitive, and **code-free** approach to building +data pipelines. It allows users to create **powerful** data workflows +using pre-built **blocks** that can be easily **connected**, all without +writing a single line of code. -``` r -library(blockr) -library(blockr.data) +![](man/figures/README-unnamed-chunk-2-1.png) -data_block <- new_dataset_block(selected = "lab", package = "blockr.data") +To get started, we invite you to read this +[vignette](https://blockr-org.github.io/blockr/articles/blockr.html). -stack <- new_stack(data_block) -serve_stack(stack) -``` +To get a better idea of `{blockr}` capabilities in various data context, +you can look at this +[vignette](https://blockr-org.github.io/blockr/articles/blockr_examples.html). + +## Key features + +1. **User-Friendly Interface**: Build data pipelines with intuitive + interface. +2. **Flexibility**: Easily add, remove, or rearrange blocks in your + pipeline. +3. **Extensibility**: Developers can create custom blocks to extend + functionality. +4. **Reproducibility**: Pipelines created with `blockr` are easily + shareable and reproducible, with exportable code. +5. **Interactivity**: Real-time feedback as you build and modify your + pipeline. ## Installation @@ -42,8 +47,83 @@ You can install the development version of blockr from [GitHub](https://github.com/) with: ``` r -# install.packages("devtools") -devtools::install_github("blockr-org/blockr") +pak::pak("blockr-org/blockr") +``` + +## Example: palmer penguins case study + +Below is a simple case study involving `{blockr}`. We use the +palmerpenguins dataset to find out which femal species has the largest +flippers. We create 2 custom blocks allowing to create our plot block +(see the plot vignette for more details). Note that the +`{blockr.ggplot2}` package exposes some ready to use blocks. + +
+ +
+ + + +
+ + + + + + + +
+ +``` r +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"), + ... + ) +} + +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) ``` ## Contribute diff --git a/man/figures/README-unnamed-chunk-2-1.png b/man/figures/README-unnamed-chunk-2-1.png new file mode 100644 index 00000000..8a47c897 Binary files /dev/null and b/man/figures/README-unnamed-chunk-2-1.png differ diff --git a/man/update_field.Rd b/man/update_field.Rd index 262fbcb9..5f6e0960 100644 --- a/man/update_field.Rd +++ b/man/update_field.Rd @@ -4,6 +4,7 @@ \alias{update_field} \alias{update_field.field} \alias{update_field.hidden_field} +\alias{get_field_name} \title{Update field generic} \usage{ update_field(x, new, env = list()) @@ -11,6 +12,8 @@ update_field(x, new, env = list()) \method{update_field}{field}(x, new, env = list()) \method{update_field}{hidden_field}(x, new, env = list()) + +get_field_name(field, name = "") } \arguments{ \item{x}{An object inheriting form \code{"field"}} @@ -18,6 +21,10 @@ update_field(x, new, env = list()) \item{new}{Value to set} \item{env}{Environment with data and other field values} + +\item{field}{Field element.} + +\item{name}{To pass a name if no title attribute.} } \value{ The modified field. diff --git a/vignettes/.gitignore b/vignettes/.gitignore index 097b2416..9e2bd63c 100644 --- a/vignettes/.gitignore +++ b/vignettes/.gitignore @@ -1,2 +1,4 @@ *.html *.R + +/.quarto/ diff --git a/vignettes/blockr.Rmd b/vignettes/blockr.Rmd index f2609385..4bd31f29 100644 --- a/vignettes/blockr.Rmd +++ b/vignettes/blockr.Rmd @@ -30,6 +30,10 @@ note_box <- function(..., color) { } ``` +```{r echo=FALSE} +knitr::include_url("https://blockr-org.github.io/useR2024") +``` + ## Get started `{blockr}` provides plug and play blocks which can be used to import, transform and visualize data. @@ -42,21 +46,59 @@ note_box <- function(..., color) { - __transform__ block: materialize `{dplyr}` operations on the selected data. - __output__ (only plot for now) block: consumes transformed data to produce visualizations. -A __stack__ always starts from a data block and may end either by a transform blocks or an output block. - -Note: at the moment, there is only one output block per stack. +A __stack__ always starts from a data block and may end either by a transform blocks or an output block. This means that, at the moment, there can only be one output block per stack. ```{r, echo=FALSE} mermaid(" - flowchart TB - subgraph stack - input(Data); - transform(Transform); - output(Visualize); - input-->transform--> output; + flowchart TD + subgraph stack1[Stack] + direction TB + subgraph input_block[Block 1] + input(Data: dataset, browser, ...) + end + subgraph transform_block[Block 2] + transform(Transform block: filter, select ...) + end + subgraph output_block[Block 3] + output(Result/transform: plot, filter, ...) + end + input_block --> |data| transform_block --> |data| output_block + end + ", + height = "600px" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` + +Under the hoods, blocks are composed of __fields__. The latter are translated into shiny inputs used to convey interactivity within the block. As you can see in the below diagram, fields are combined to compute an expression, which eventually leads to a block result after evaluation. For a data block, there is no data input. + +```{r, echo=FALSE} +mermaid( + "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 ", - height = "120px" + height = "800px" ) |> htmlwidgets::onRender( "function(el, x) { @@ -66,6 +108,47 @@ mermaid(" ) ``` +For instance, if we assume a filter `{dplyr}` __expression__: + +```{r, eval=FALSE} +data |> filter(col1 == "test") +``` + +This operation can be translated to `data |> filter( , ...)`, where ``, `` and `` are __fields__, to give the end user the ability to change the +expression. Note that fields may have dependencies, `` controling the number of filters to apply. Besides, at the moment, our implementation of the filter block exposes additive operations, meaning that the result corresponds to `cond1 && cond2 && ...`. + +```{r, echo=FALSE} +mermaid( + "flowchart TD + filt_data_in[Data input] + filt_data_out[Transformed data] + subgraph filt_block[Filter block] + filt_select_col[Select columns] + filt_filter_func[Filter function ==, !=] + filt_filter_val[Filter values] + filt_expr[Expression] + filt_res[Result] + + filt_select_col --> filt_expr + filt_select_col --> |depends| filt_filter_val + filt_filter_val --> filt_expr + filt_filter_func --> filt_expr + + filt_expr --> filt_res + end + filt_data_in --> filt_block --> filt_data_out + ", + height = "700px" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` + + ### The user interface By default, only the last __stack__ block is visible, all others are collapsed. @@ -241,3 +324,36 @@ server <- function(input, output, session) { shinyApp(ui, server) ``` + +### Connect stacks: the workspace + +The __workspace__ allows you to create more complex analysis by connecting stacks together and get a dashboard. +To know more about this you can read the following [article](https://blockr-org.github.io/blockr/articles/data-blocks.html#reading-data-from-another-stack). + +```{r, echo=FALSE} +mermaid( + "flowchart TD + subgraph s1[Stack 1] + direction TB + input_s1(Data) + transform_s1(Transform) + input_s1 --> |data| transform_s1 + end + transform_s1 --> |data| input_s2 + subgraph s2[Stack 2] + direction TB + input_s2(Data) + transform_s2(Transform) + output_s2(Visualize) + input_s2 --> |data| transform_s2 --> |data| output_s2 + end + ", + height = "600px" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` diff --git a/vignettes/blockr_examples.Rmd b/vignettes/blockr_examples.Rmd new file mode 100644 index 00000000..8784c459 --- /dev/null +++ b/vignettes/blockr_examples.Rmd @@ -0,0 +1,820 @@ +--- +title: "Case studies" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Case studies} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include=FALSE} +library(blockr) +library(bslib) +knitr::opts_chunk$set(echo = TRUE) +``` + +## blockr Across Industries + +The flexibility of `blockr` makes it valuable across various industries. Let's explore how it can be applied in different sectors with detailed examples. Somex examples require to create a new field such as the `new_slider_field` described in the corresponding vignette. + +```{r slider_block, include=FALSE} +# Provided by John, new_range_field not working +new_slider_field <- function(value = 5L, min = 0L, max = 10L, step = 1L, ...) { + blockr::new_field( + value = value, + min = min, + max = max, + step = step, + ..., + class = "slider_field" + ) +} + +#' @method ui_input slider_field +#' @export +ui_input.slider_field <- function(x, id, name) { + shiny::sliderInput( + blockr:::input_ids(x, id), + name, + value = blockr::value(x, "value"), + min = blockr::value(x, "min"), + max = blockr::value(x, "max"), + step = blockr::value(x, "step") + ) +} + +#' @method validate_field slider_field +#' @export +validate_field.slider_field <- function(x) { + x +} + +#' @method ui_update slider_field +#' @export +ui_update.slider_field <- function(x, session, id, name) { + updateSliderInput( + session, + blockr::input_ids(x, id), + blockr::get_field_name(x, name), + blockr::value(x), + blockr::value(x, "min"), + blockr::value(x, "max") + ) +} +``` + +```{r filter_block, include=FALSE} +#' Correlation plot +#' @param data dataset. +#' @param ... ignored. +#' @export +filter_in_block <- function(data, ...) { + sub_fields <- \(data, column) { + if (!length(column)) { + column <- names(data)[1] + } + + list( + values = blockr::select_field(data[[column]][1], unique(data[[column]]), multiple = TRUE) + ) + } + + fields <- list( + column = blockr::new_select_field( + \(data) names(data)[1], + choices = \(data) names(data), + title = "Column" + ), + values = blockr::new_list_field(character(), sub_fields) + ) + + blockr::new_block( + fields = fields, + expr = quote({ + vals <- unname(.(values)) + data |> + dplyr::filter(!!dplyr::sym(.(column)) %in% vals[[1]]) + }), + ..., + class = c("filter_in_block", "transform_block", "submit_block") + ) +} + +blockr::register_block( + constructor = filter_in_block, + name = "Filter in", + description = "Filter on a vector", + classes = c("filter_in_block", "transform_block"), + input = "data.frame", + output = "data.frame" +) +``` + + +### 1. Finance: Stock Price Forecasting + +In this example, we'll create a pipeline that fetches recent stock data using the [quantmod package](https://github.com/joshuaulrich/quantmod), performs time series analysis, and forecasts future stock prices using the [Prophet model](https://github.com/facebook/prophet). We first design the `stock_data_block`, containing a field to select the stock items and generate the data. The `prophet_forecast_block` does all the modeling part. + +```{r stock_example, eval=FALSE} +# Does not run on shinylive as quantmod/prophet not available +library(blockr) +library(quantmod) +library(prophet) + +# Custom block to fetch stock data +new_stock_data_block <- function(...) { + # stocks to pick (top 10) + pick_stock <- \() c("NVDA", "TSLA", "AAPL", "MSFT", "AVGO", "AMZN", "AMD", "PLTR", "TSM", "META") + + + new_block( + fields = list( + ticker = new_select_field(pick_stock()[1], pick_stock, multiple = FALSE, title = "Ticker") + ), + expr = quote({ + data_xts <- getSymbols(.(ticker), src = "yahoo", auto.assign = FALSE) + data.frame(Date = index(data_xts), coredata(data_xts)) |> + tail(700) # only considering last 700 days for this example + }), + class = c("stock_data_block", "data_block"), + ... + ) +} + +# Custom block for Prophet forecasting +new_prophet_forecast_block <- function(columns = character(), ...) { + all_cols <- function(data) colnames(data)[2:length(colnames(data))] + + + new_block( + fields = list( + # date_col = new_select_field(columns, all_cols, multiple=FALSE, title="Date"), + value_col = new_select_field(columns, all_cols, multiple = FALSE, title = "Value"), + periods = new_slider_field(7, min = 0, max = 365, title = "Forecast duration") + ), + expr = quote({ + df <- data.frame( + ds = data$Date, + y = data[[.(value_col)]] + ) + model <- prophet(df) + future <- make_future_dataframe(model, periods = .(periods)) + forecast <- predict(model, future) + plot(model, forecast) + }), + class = c("prophet_forecast_block", "plot_block"), + ... + ) +} + +# Register custom blocks +register_block( + new_stock_data_block, + name = "Stock Data", + description = "Fetch stock data", + classes = c("stock_data_block", "data_block"), + input = NA_character_, + output = "data.frame" +) +register_block( + new_prophet_forecast_block, + name = "Prophet Forecast", + description = "Forecast using Prophet", + classes = c("prophet_forecast_block", "plot_block"), + input = "data.frame", + output = "plot" +) + +# Create the stack +stock_forecast_stack <- new_stack( + new_stock_data_block(), + new_prophet_forecast_block() +) + +serve_stack(stock_forecast_stack) +``` + +```{r blockr-stock, echo=FALSE, fig.cap='Stock model demo', fig.align = 'center', out.width='100%'} +knitr::include_graphics("figures/blockr-stock-demo.png") +``` + +### 2. Pharmaceutical: Clinical Trial Analysis + +#### 2.1 AE Forest Plot + +This forest plot visualizes the relative risk of adverse events between two treatment arms in a clinical trial. In this case, it compares "Xanomeline High Dose" to "Xanomeline Low Dose" starting from the [pharmaverseadam](https://github.com/pharmaverse/pharmaverseadam) `adae` dataset. As you may notice, the `new_forest_plot_block` is a quite complex block. Part of the code is isolated in a function `create_ae_forest_plot` so that the main block constructor is more readable. + +```{r, eval=TRUE, echo=FALSE} +card( + create_app_link( + "NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAdzgCMAnRRASwgGdSoAbbgCgA6YOtyIEA1gyG4ABAzioi7GQF4ZBQWAAWpUqnaIA9IZFjJAWiIMA5hmstSWgK50MLIodqNz8xe2kyQjp6Bsa+RBgM5tRQ7DAYVtZCAJTJAhBeTKwcXLyaphIMGKhaUAwwUABucAzscFAAJrAB4cpqGkG6+kYmooWWNnYOzq7unvRRrQGdIT3hkdGx8YkpaRDp3CyMZQCefAWSa5vbDHsNqNw7DEdbDLt8pCwNVzcnewBmVnCcF0Skr3dTvs+pJiqVylUanVGrA1ukAMQyABiTggBEeJBkpCI6nkUHIMka1VqcBkcGqZBkn3knBkv1I6QIePIAH0oHAWdTvqQWfSZAAecxU1Ho9wQPhNLiyJzsFwAKyeLOI3FkZRgSqIKsJcAacGIDQ1WogLPJFFI7GSMhA6RkMklUAFQvtMgAPgA+KksbjkBh8DD24DANWGgC6IZkAEI1EIAArcKAEehEFI2mSIgDCJGJpHUmqcMAgMmg8GU2JknAYbGs-kLMhl8sVysdhPYGAI4ITPr4sronAcTnIfHrdAVBuVqVTwabgpbbY76Jq3ZcfdIA7gfCnmontfZuv1hubsTnZU7i57K7XG51eqIY63cNrGZ4BCc8YJjlJ2NyRfzdBqMiId5y3lPVzRkNgyQTLRCXKVNjR7OVQOUGdnXdVNbTqbhQI3XgWUAvgNGHUdDVVcpDVSS00NrW0GhYPs0VIPhKLddCZGsBgiCcVAWToPYEw49h2Bw7g8PeDcyPHVJXRY6jgJgCpKzqPhCzUcVklkOwOK4tpAjABoONQFN1kfGR02fV98VJABBABRKl5AARycCgCBYb5CQgBo6QMqwMQ4ScOVleTdmbVCZNtW12M47jeI3JklCEngRPwjRNy1XcbzvbgKOY1igoUuj12NU0yB041aPo9E-QDYAiMbTUw3UmRNOinShH0ohDLAXLZKw94eTlIg2GUlkEKQ2ReNUGD1XHaTWJgAdLL4VAfIYPypuKikwMMIsH1tREAGU4Cw9EsU6mQADkZBgJQc3eRznMpWzlH4hLCV4aaa1tbFuPZZChXZUb8wUnY5tkqKuJ4vj4sE4TRKvPdb3IqSqIiuSCqUr8eBNLadKCkaSvNJqWu0qb2oMlIwbRn6WXFTazXYWQsZEwmLSpiLUFffgkvhjL90k9JU0RABxOAc1RFgnM-Zl4EpNUvs+5sJaliV8SgQM0sa1MWCAvgsIgaxHHEmA2ajGQACZLWtWTOE6zQABUtDoulRBzB6WBpMkAA9O0uLFqBxUgZbNT6MCM20AF9J1Fao2XKQ9ynYYAAEYQ1TYgyA4kS1QTk3gHNtPjP25EvR9KkrDO1AZGejyvIIcy31JeQ3xYao5Do8QApZBgGEPQLgZC1GIveUvF39NWNevfmGvDABSNhZ8rtlvh6tHUFbv4WWoJ5F1Y216u4HSefwvmkfHXA96LWBvk5DiYCm4+xLS9TL8qHhnPYW+iHv9oCdx2QICyBWp1XyYo1ho1XhFBaXBByXywFgKa48uCBlQLEcgAAGTQwDFBrTFCyAInZW4cjVMkMMMgdpIPVsAVBnA4CYKENg0BJB8F4FzJnTUccYCkJDBfWStpRC0AYEqFgU04Be1QHrIg1g+DwMtEKZOGAACcAA2GQAAqcsDk1qCD4RFZO5Dmo1RoRgzQ9NSosNVDHYh5RuEyAANSXzRvoihRi0F0NMTjBmFi2FBw4SQsh5hHF6IMZQlBbj6GQG8YQ2O-jwyBN0U4kJrjaERONAEDOvjs42LDJfVIvC0a2i4qgGowjRHiMkdI2R9iZAKJUeozR2ignOMMRPah4SPGsyiVYzhtiHEJOCS41pxj3FCDMeabxGSs49ICUE20zTQltJSR4gh3TYkyHiQUgp8zkkmNGRMkgmTpmF02duCBgsTIxl8GUUkzpqTlxpDmekqYuScBZM6GcgMe7s1tNApal94x-m4FNYZ9D+kLNPllMMsghAyE0PkzZLTkGLN2ZEzpKzHgxOyTw3ShgAgLJBcs1h0TrFcKhbpGQlRlDSCCfi9pey0WsMmX4rF0KwC4tYbSpZez0kHKmbE1laQwC5NYntNMplmSkheY812zyvg-FdjotGALjrkC9jmdodA2ANEVQUqVby1YABJlUqkvuwFabBSBiSELPDA5tdY2rteYB17xBWyD1faA18C3Vyp5B6gRJTXLeoefqrgBqikBpYOAiKL9ZLwCgCpe53IQ1QE9VgeF-CiCCKmu6w1-qhGBtYuG3uagc2hqLcI9N70WDWATR0MA3BphgCkN1St7E0BaG3g0RwU0JaMWUeg1l8kUiVsTF7Ka6ClGVs2BAOAWg4DVp0D2iADg+AAA5B0wGHaxOgRAvbsBYAAL1JGoCdABmAArCOzU2bUCZlELUfYu6yZNqIDsHgIhnIBGnce3STQGDiA-XAAI+UQrRhfW+7ggGt2yVIGqlk1gq4ltQPbNVwsJHGqmghsoBExHjqUU1R4EgdJYd9KOvDiimpewBZh1BpHcMnvwzGtGVGoB0GBW4zQWBjr4iITILAHdYXoqIZw1lBieXsKyTAAV0G0ZHo4lNZOU7qANAwHJnEahzZKZU65KamnWJqpYERqaGgJ2XpqbITTMgAAsTGIrsSeFNe2WAACqNlK2PFIFhdjtDOPcYxaSfj7BxAASAlZBoxI6gyBsrjQTRLVnlFE5S3SsgmWSek91VMawo7GRndQTkPreSux4iCZs7wRR+T9BgDAVs4JwDywcHVtoR7HQaDpTYnBGsRTqllDadXRrHVApyNy3BtVBKEM5g6zmABCAApAAkgAEWpf0srDExSqy4JaZUxZvgbagLZzZC1vQsAuD+pEVkAAyB03NBI815sDB0QKnUW6ZPMBYhC5MrbaNKvW8uYUG81kbnW0ZCCsume2VksAAFllsIuFGtkge2tuah20Je0B2ClHceKdqa52rs3f6Xdn9Qh7bByevHO9+Z1hCt0Rj77U8z7XtUn1-76IhsteBxFUHNkFs2XTAAeSW3gIJq3RSI-R7mbgqOkdfaga+bH93kSXeu7L76DhFfc5kPbGo99KfvZpyc2XYyyp9YgPmGohn2dA6aQOoJMAIJqDMxUMdGmB3lnIIh8zt31fE7ABdX8-5AJa-OjXMsC26IXCgDsD7tORWVrEStKaTk-jrmtpspk9RWSAz1fSPbGkhwIQPsTY25F88QtL81P+DNTkRQjhjgg8ZBLGc0ESKEHJCaFc3gcRt9JitmBkxFKrYdaxZfOYiLj9haG9w-OoGU2J77d4gPISfPo+8SB1bl-Lwbe8HHhTt59YWIukmiyHJEPqZBxj+LDu03wmQnfWmB9MErCSJtpHyIPreSRkhi8QGAtGqz+yBxk45jyzX4N6xB1A6R1qf51CeJkCd48jd6sIMJFZIG2ZsCczqq-pqwYD3TXzX6cR6ADjPpPI04PgZjP4z6cAJidxojTqGbYxBwsDYzUESDNib6sHiAb59YwjqgNas7kBeRgYwhAYtpCyejeglJsBr5cG2ab456oEghMTpAPh1AMCxycEET0H1wiRMEsFcASDJBgARwhhAA", # nolint + mode = "editor" + ), + full_screen = TRUE +) +``` + +

+ +Toggle code + +

+ +
+ +```r +library(dplyr) +library(tidyr) +library(forestplot) +library(blockr.pharmaverseadam) + +# Function to create adverse event forest plot +create_ae_forest_plot <- function(data, usubjid_col, arm_col, aedecod_col, n_events) { + data <- data |> filter(.data[[arm_col]] != "Placebo") + # Convert column names to strings + usubjid_col <- as.character(substitute(usubjid_col)) + arm_col <- as.character(substitute(arm_col)) + aedecod_col <- as.character(substitute(aedecod_col)) + + # Calculate the total number of subjects in each arm + n_subjects <- data |> + select(all_of(c(usubjid_col, arm_col))) |> + distinct() |> + group_by(across(all_of(arm_col))) |> + summarise(n = n(), .groups = "drop") + + # Calculate AE frequencies and proportions + ae_summary <- data |> + group_by(across(all_of(c(arm_col, aedecod_col)))) |> + summarise(n_events = n_distinct(.data[[usubjid_col]]), .groups = "drop") |> + left_join(n_subjects, by = arm_col) |> + mutate(proportion = n_events / n) + + # Select top N most frequent AEs across all arms + top_aes <- ae_summary |> + group_by(across(all_of(aedecod_col))) |> + summarise(total_events = sum(n_events), .groups = "drop") |> + top_n(n_events, total_events) |> + pull(all_of(aedecod_col)) + + # Get unique treatment arms + arms <- unique(data[[arm_col]]) + if (length(arms) != 2) { + stop("This plot requires exactly two treatment arms.") + } + active_arm <- arms[1] + control_arm <- arms[2] + + # Filter for top AEs and calculate relative risk + ae_rr <- ae_summary |> + filter(.data[[aedecod_col]] %in% top_aes) |> + pivot_wider( + id_cols = all_of(aedecod_col), + names_from = all_of(arm_col), + values_from = c(n_events, n, proportion) + ) |> + mutate( + RR = .data[[paste0("proportion_", active_arm)]] / .data[[paste0("proportion_", control_arm)]], + lower_ci = exp(log(RR) - 1.96 * sqrt( + 1 / .data[[paste0("n_events_", active_arm)]] + + 1 / .data[[paste0("n_events_", control_arm)]] - + 1 / .data[[paste0("n_", active_arm)]] - + 1 / .data[[paste0("n_", control_arm)]] + )), + upper_ci = exp(log(RR) + 1.96 * sqrt( + 1 / .data[[paste0("n_events_", active_arm)]] + + 1 / .data[[paste0("n_events_", control_arm)]] - + 1 / .data[[paste0("n_", active_arm)]] - + 1 / .data[[paste0("n_", control_arm)]] + )) + ) + + # Prepare data for forest plot + forest_data <- ae_rr |> + mutate( + label = paste0( + .data[[aedecod_col]], " (", + .data[[paste0("n_events_", active_arm)]], "/", .data[[paste0("n_", active_arm)]], " vs ", + .data[[paste0("n_events_", control_arm)]], "/", .data[[paste0("n_", control_arm)]], ")" + ) + ) + + # Create forest plot + forestplot::forestplot( + labeltext = cbind( + forest_data$label, + sprintf("%.2f (%.2f-%.2f)", forest_data$RR, forest_data$lower_ci, forest_data$upper_ci) + ), + mean = forest_data$RR, + lower = forest_data$lower_ci, + upper = forest_data$upper_ci, + align = c("l", "r"), + graphwidth = unit(60, "mm"), + cex = 0.9, + lineheight = unit(8, "mm"), + boxsize = 0.35, + col = fpColors(box = "royalblue", line = "darkblue", summary = "royalblue"), + txt_gp = fpTxtGp(label = gpar(cex = 0.9), ticks = gpar(cex = 0.9), xlab = gpar(cex = 0.9)), + xlab = paste("Relative Risk (", active_arm, " / ", control_arm, ")"), + zero = 1, + lwd.zero = 2, + lwd.ci = 2, + xticks = c(0.5, 1, 2, 4), + grid = TRUE, + title = paste("Relative Risk of Adverse Events (", active_arm, " vs ", control_arm, ")") + ) +} + +new_forest_plot_block <- function(...) { + new_block( + fields = list( + usubjid_col = new_select_field( + "USUBJID", + function(data) colnames(data), + multiple = FALSE, + title = "Subject ID Column" + ), + arm_col = new_select_field( + "ACTARM", + function(data) colnames(data), + multiple = FALSE, + title = "Treatment Arm Column" + ), + aedecod_col = new_select_field( + "AEDECOD", + function(data) colnames(data), + multiple = FALSE, + title = "AE Term Column" + ), + n_events = new_numeric_field( + 10, + min = 5, max = 20, step = 1, + title = "Number of Top AEs to Display" + ) + ), + expr = quote({ + create_ae_forest_plot(data, .(usubjid_col), .(arm_col), .(aedecod_col), .(n_events)) + }), + class = c("adverse_event_plot_block", "plot_block"), + ... + ) +} + +# Register the custom block +register_block( + new_forest_plot_block, + name = "Adverse Event Forest Plot", + description = "Create a forest plot of adverse events comparing two treatment arms", + classes = c("adverse_event_plot_block", "plot_block"), + input = "data.frame", + output = "plot" +) + +# Create the stack +clinical_trial_stack <- new_stack( + new_adam_block(selected = "adae"), + # filter_in_block(), + new_forest_plot_block() +) + +serve_stack(clinical_trial_stack) +``` +
+ +#### 2.2 Demographics Table + +This demographics table is taken from the `{cardinal}` package of [FDA Safety Tables and Figures](https://github.com/pharmaverse/cardinal) and demonstrates `{gt}` and `{rtables}` outputs starting from the [pharmaverseadam](https://github.com/pharmaverse/pharmaverseadam) `adsl` dataset. As a side note, the below block requires some extra helpers to work properly which you can find [here](https://github.com/blockr-org/blockr.cardinal/blob/72e6e5518d8226d4fdeb599b6e2e31ba03dc7c82/R/methods.R) in the `{blockr.cardinal}` package. + +```{r cardinal_tables, eval=FALSE} +library(shiny) +library(blockr) +library(cardinal) +library(blockr.pharmaverseadam) + +new_cardinal02_block <- function(...) { + all_cols <- function(data) colnames(data) + + fields <- list( + columns = new_select_field( + c("SEX", "AGE", "AGEGR1", "RACE", "ETHNIC", "COUNTRY"), + all_cols, + multiple = TRUE, + title = "Variables" + ) + ) + + expr <- quote({ + data <- droplevels(data) + + rtables <- cardinal::make_table_02( + df = data, + vars = .(columns) + ) + + gt <- cardinal::make_table_02_gtsum( + df = data, + vars = .(columns) + ) + + list( + rtables = rtables, + gt = gt + ) + }) + + new_block( + expr = expr, + fields = fields, + ..., + class = c("cardinal02_block", "rtables_block", "submit_block") + ) +} + +register_block( + new_cardinal02_block, + "Cardinal 02", + "A Cardinal 02 table", + input = "data.frame", + output = "list", + classes = c("cardinal02_block", "rtables_block", "submit_block") +) + + +# Create the stack +rtables_stack <- new_stack( + new_adam_block(selected = "adsl"), + new_cardinal02_block() +) + +serve_stack(rtables_stack) +``` + +```{r blockr-cardinal, echo=FALSE, fig.cap='Cardinal demo', fig.align = 'center', out.width='100%'} +knitr::include_graphics("figures/blockr-cardinal-demo.png") +``` + +### 3. Environmental Science: Air Quality Analysis and Prediction + +This example demonstrates a pipeline for analyzing air quality data and predicting future pollution levels using actual data from the [openair](https://github.com/cran/openair) package. +This pipeline imports actual air quality data from the `{openair}` package and forecasts future pollution levels using an ARIMA model. + +```{r environment, eval=FALSE} +library(blockr) +library(openair) +library(forecast) + +# Custom block for air quality data import +new_air_quality_block <- function(...) { + new_block( + fields = list( + site = new_select_field( + "kc1", + \() openair::importMeta()$code, + multiple = FALSE, + title = "Monitoring Site" + ), + start_year = new_numeric_field( + 2020, + min = 1990, + max = as.numeric(format(Sys.Date(), "%Y")), + step = 1, + title = "Start Year" + ), + end_year = new_numeric_field( + as.numeric(format(Sys.Date(), "%Y")), + min = 1990, + max = as.numeric(format(Sys.Date(), "%Y")), + step = 1, + title = "End Year" + ) + ), + expr = quote({ + importAURN(site = .(site), year = .(start_year):.(end_year)) |> tail(700) + }), + class = c("air_quality_block", "data_block"), + ... + ) +} + + +# Custom block for pollution forecasting +new_pollution_forecast_block <- function(columns = character(), ...) { + all_cols <- function(data) setdiff(colnames(data), c("date", "site", "source")) + + new_block( + fields = list( + pollutant = new_select_field(columns, all_cols, multiple = FALSE, title = "Pollutant"), + horizon = new_slider_field( + 30, + min = 1, + max = 365, + step = 1, + title = "Forecast Horizon (days)" + ) + ), + expr = quote({ + ts_data <- ts(na.omit(data[[.(pollutant)]]), frequency = 365) + model <- auto.arima(ts_data) + forecast_result <- forecast(model, h = .(horizon)) + plot(forecast_result, main = paste("Forecast of", .(pollutant), "levels")) + }), + class = c("pollution_forecast_block", "plot_block"), + ... + ) +} + +# Register custom blocks +register_block( + new_air_quality_block, + name = "Air Quality Data", + description = "Import air quality data", + classes = c("air_quality_block", "data_block"), + input = NA_character_, + output = "data.frame" +) + +register_block( + new_pollution_forecast_block, + name = "Pollution Forecast", + description = "Forecast pollution levels", + classes = c("pollution_forecast_block", "plot_block"), + input = "data.frame", + output = "plot" +) + +# Create the stack +air_quality_stack <- new_stack( + new_air_quality_block(), + new_pollution_forecast_block(columns = "no2") +) + +serve_stack(air_quality_stack) +``` + +### 4. Marketing: Causal Impact Analysis of Marketing Interventions + +This example demonstrates how to use [CausalImpact](https://google.github.io/CausalImpact/CausalImpact.html) to analyze the effect of marketing interventions on sales data. +This pipeline generates dummy marketing data with an intervention, then uses `{CausalImpact}` to analyze the effect of the intervention on sales. This requires to define a new date field as shown below. + +```{r date_field} +library(shiny) +new_date_field <- function(value = Sys.Date(), min = NULL, max = NULL, ...) { + blockr::new_field( + value = value, + min = min, + max = max, + ..., + class = "date_field" + ) +} + +date_field <- function(...) { + validate_field(new_date_field(...)) +} + +#' @method ui_input date_field +#' @export +ui_input.date_field <- function(x, id, name) { + shiny::dateInput( + blockr::input_ids(x, id), + name, + value = blockr::value(x, "value"), + min = blockr::value(x, "min"), + max = blockr::value(x, "max") + ) +} + +#' @method validate_field date_field +#' @export +validate_field.date_field <- function(x, ...) { + x +} + +#' @method ui_update date_field +#' @export +ui_update.date_field <- function(x, session, id, name) { + updateDateInput( + session, + blockr::input_ids(x, id), + blockr::get_field_name(x, name), + value = blockr::value(x), + min = blockr::value(x, "min"), + max = blockr::value(x, "max") + ) +} +``` + +```{r intervention_impact, eval=FALSE} +library(blockr) +library(CausalImpact) +library(dplyr) + +# Custom block to load and prepare marketing data +new_marketing_data_block <- function(...) { + new_block( + fields = list( + start_date = date_field( + Sys.Date() - 365, + min = Sys.Date() - 730, + max = Sys.Date() - 1, + label = "Start Date" + ), + intervention_date = date_field( + Sys.Date() - 180, + min = Sys.Date() - 729, + max = Sys.Date(), + label = "Intervention Date" + ), + end_date = date_field( + Sys.Date(), + min = Sys.Date() - 364, + max = Sys.Date(), + label = "End Date" + ) + ), + expr = quote({ + # Generate dummy data for demonstration + dates <- seq(as.Date(.(start_date)), as.Date(.(end_date)), by = "day") + sales <- cumsum(rnorm(length(dates), mean = 100, sd = 10)) + ad_spend <- cumsum(rnorm(length(dates), mean = 50, sd = 5)) + + # Add intervention effect + intervention_index <- which(dates == as.Date(.(intervention_date))) + sales[intervention_index:length(sales)] <- sales[intervention_index:length(sales)] * 1.2 + + data.frame( + date = dates, + sales = sales, + ad_spend = ad_spend + ) + }), + class = c("marketing_data_block", "data_block"), + ... + ) +} + +# Custom block for CausalImpact analysis +new_causal_impact_block <- function(columns = character(), ...) { + all_cols <- function(data) colnames(data)[2:length(colnames(data))] + + new_block( + fields = list( + response_var = new_select_field( + columns, + all_cols, + multiple = FALSE, + title = "Response Variable" + ), + covariate_var = new_select_field( + columns, + all_cols, + multiple = FALSE, + title = "Covariate Variable" + ), + pre_period_end = date_field( + Sys.Date() - 181, + min = Sys.Date() - 729, + max = Sys.Date() - 1, + label = "Pre-Period End Date" + ), + post_period_start = date_field( + Sys.Date() - 180, + min = Sys.Date() - 728, + max = Sys.Date(), + label = "Post-Period Start Date" + ) + ), + expr = quote({ + data <- data.frame( + date = data$date, + y = data[[.(response_var)]], + x = data[[.(covariate_var)]] + ) + pre_period <- c(min(as.Date(data$date)), as.Date(.(pre_period_end))) + post_period <- c(as.Date(.(post_period_start)), max(as.Date(data$date))) + + impact <- CausalImpact(data, pre_period, post_period) + plot(impact) + }), + class = c("causal_impact_block", "plot_block"), + ... + ) +} + +# Register custom blocks +register_block( + new_marketing_data_block, + name = "Marketing Data", + description = "Load and prepare marketing data", + classes = c("marketing_data_block", "data_block"), + input = NA_character_, + output = "data.frame" +) +register_block( + new_causal_impact_block, + name = "Causal Impact Analysis", + description = "Perform Causal Impact analysis on marketing data", + classes = c("causal_impact_block", "plot_block"), + input = "data.frame", + output = "plot" +) + +# Create the stack +marketing_impact_stack <- new_stack( + new_marketing_data_block(), + new_causal_impact_block() +) + +serve_stack(marketing_impact_stack) +``` + +```{r blockr-marketing, echo=FALSE, fig.cap='Marketing demo', fig.align = 'center', out.width='100%'} +knitr::include_graphics("figures/blockr-marketing-demo.png") +``` + +### 5. Dynamical systems + +In the below example, we implemented the Lorenz attractor and solve it with the `{pracma}` R package (technically, the reason using `{pracma}` over `{deSolve}` or `{diffeqr}` is because only `{pracma}` is available for shinylive required by the embeded demo). + +```{r, eval=TRUE, echo=FALSE} +card( + create_app_link( + "NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAdzgCMAnRRASwgGdSoAbbgCgA6YOtyIEA1gyG4ABAzioi7GQF4ZBQWAAWpUqnaIA9IZFjJAWiIMA5hmstSWgK50MLIodqNz8xe2kyQjp6Bsa+RBgM5tRQ7DAYVtZCAJTJAhBeTKwcXLyaphIMdtaooqQATAHhymoaQbr6RiaihZY2dg7Oru6e9FHVAfUhTeGR0bHxiSlpEOncLIxQDACefAWSM-OLK3yoDFAEMFCbC-s760XWJWXlM+kQcNQA+kQAJnBP6zIAPOYyAGZOCAEUjuCB8DCQ5IyEDpdIyGSieQQABePz+gOBoJIMj4pFky1kqCWMHY0NhEARCLqlKpCIAGqoZMSGKTgABGAC6MgAVDJlhzuQBqfnAcrcvkCgDMnNw8LpMgAmkyWWzxbzcQL1X9pZzknLaXSAFpM8wCrkarXC5kk9jAGWWsXcnX2znyhEzBEAXzhtP+LDg3Feyl+iJYnEEhpkUCZD2eECc8AYLAIT39gdefHMAA5DFLZOZ2QAGWTlIv690yOixx5PBNJlNpgNBrPFgsAVhLMmL5YNdIINfjibgydT6Zb5WzsnZ06L5fld1pcc+LXEkbp4+DTM37D7VLgAA89kz2C5OA4nOR1wrYhhXlAuBh-vt4NeFTI3nAACztt-vgFArIlbvssRZMhojJqDOSpMtBJpQRWUbvqQYFqCWwEKqQ-rQNwsFznu-42qyNTqBCfDHLIGBrPqMhUQQqQYR6GGIe+8wRpiTJIhQKKelSLFUpCGAEeo3CxCRdRgJ+K5mIMYD3lw0kSCkC7pD6sw5Ac4jojIy6cJp17LlJ6xAUutZXKURCkIpa6VpiA61JoB6ycsKQmXS7z-FATjcFZxDcImHDgZoywYOyzkYJUYC8TR8rLtYcBEDAihsFZ6zpDM7AjgAbh8ekSHweXiMkYBepyQA", # nolint + mode = "editor" + ), + full_screen = TRUE +) +``` + +

+ +Toggle code + +

+ +
+ +```r +library(blockr) +library(pracma) +library(blockr.ggplot2) + +new_ode_block <- function(...) { + + lorenz <- function (t, y, parms) { + c( + X = parms[1] * y[1] + y[2] * y[3], + Y = parms[2] * (y[2] - y[3]), + Z = -y[1] * y[2] + parms[3] * y[2] - y[3] + ) + } + + fields <- list( + a = new_numeric_field(-8/3, -10, 20), + b = new_numeric_field(-10, -50, 100), + c = new_numeric_field(28, 1, 100) + ) + + new_block( + fields = fields, + expr = substitute( + as.data.frame( + ode45( + fun, + y0 = c(X = 1, Y = 1, Z = 1), + t0 = 0, + tfinal = 100, + parms = c(.(a), .(b), .(c)) + ) + ), + list(fun = lorenz) + ), + ..., + class = c("ode_block", "data_block") + ) +} + +stack <- new_stack( + new_ode_block, + # Coming from blockr.ggplot2 + new_ggplot_block( + func = c("x", "y"), + default_columns = c("y.1", "y.2") + ), + new_geompoint_block +) +serve_stack(stack) +``` +
diff --git a/vignettes/developer-guide.Rmd b/vignettes/developer-guide.Rmd index 49491cfe..6be8b159 100644 --- a/vignettes/developer-guide.Rmd +++ b/vignettes/developer-guide.Rmd @@ -123,7 +123,7 @@ for checking that the value is a character string and modifies it if not. TBD -## Validation and evaluation +# Validation and evaluation That's currently how {blockr} validate blocks and fields. Each block is validated, meaning that each field that compose it should have valid values according to the input data. Only when everything is valid, the block expression is evaluated and the result computed and passed to the next block. Any error is catched by R and forwarded to JavaScript so that users are notified about what went wrong and where. @@ -156,7 +156,7 @@ flowchart TD; subgraph ui gui[User interface] end -", height = "1200px") |> +", height = "1250px") |> htmlwidgets::onRender(" function(el, x) { el.classList.add('text-center') diff --git a/vignettes/figures/blockr-cardinal-demo.png b/vignettes/figures/blockr-cardinal-demo.png new file mode 100644 index 00000000..1e014bbd Binary files /dev/null and b/vignettes/figures/blockr-cardinal-demo.png differ diff --git a/vignettes/figures/blockr-marketing-demo.png b/vignettes/figures/blockr-marketing-demo.png new file mode 100644 index 00000000..fee9ad30 Binary files /dev/null and b/vignettes/figures/blockr-marketing-demo.png differ diff --git a/vignettes/figures/blockr-penguins-stack.png b/vignettes/figures/blockr-penguins-stack.png new file mode 100644 index 00000000..a12a1d6f Binary files /dev/null and b/vignettes/figures/blockr-penguins-stack.png differ diff --git a/vignettes/figures/blockr-stock-demo.png b/vignettes/figures/blockr-stock-demo.png new file mode 100644 index 00000000..c69de0bb Binary files /dev/null and b/vignettes/figures/blockr-stock-demo.png differ diff --git a/vignettes/new-field.Rmd b/vignettes/new-field.Rmd new file mode 100644 index 00000000..17f84ebd --- /dev/null +++ b/vignettes/new-field.Rmd @@ -0,0 +1,166 @@ +--- +title: "4. Case study: create a new field" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{4. Case study: create a new field} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup, include=FALSE} +library(blockr) +library(bslib) +``` + + +In this vignette, we show how to create a new field, that is the `slider_field`, similar to the range field with only 1 value. + +## Introduction + +To create a field, we first need to design a __constructor__. In our case, we call it `new_slider_field`. +Since fields are mirroring shiny inputs, we can naturally provide the corresponding input parameters that are the value, minimum, maximum and step. We pass all of these to `blockr::new_field()` and give it a custom class `slider_field`. + +```{r slider_field} +new_slider_field <- function( + value = numeric(), + min = numeric(), + max = numeric(), + step = numeric(), + ...) { + blockr::new_field( + value = value, + min = min, + max = max, + step = step, + ..., + class = "slider_field" + ) +} +``` + +On the UI side, this is fairly straightforward as all we have to do is call `shiny::sliderInput` with the same parameters given in the above constructor. + +```{r, ui-slider-field} +ui_input.slider_field <- function(x, id, name) { + shiny::sliderInput( + blockr::input_ids(x, id), + name, + value = blockr::value(x, "value"), + min = blockr::value(x, "min"), + max = blockr::value(x, "max"), + step = blockr::value(x, "step") + ) +} +``` + +The next step consists in validating the field to ensure provided values are correct. For instance, +we want to make sure all passed elements are numbers. This is done within the `validate_field.slider_field` method. We extract all the field element values with `blockr::value(x, "element")` and use the suitable validate function, that is `validate_number()`. Note that if nothing is provided, the validation falls back to `validate_field.field()`, which does nothing. + +```{r, validate-slider-field} +validate_field.slider_field <- function(x) { + val <- value(x) + min <- value(x, "min") + max <- value(x, "max") + step <- value(x, "step") + + validate_number(val) + + if (length(min)) { + validate_number(min, "min") + } + + if (length(max)) { + validate_number(max, "max") + } + + if (length(step)) { + validate_number(step, "step") + } + + NextMethod() +} +``` + +Finally, we want to make sure that we can update the field with the `ui_update.slider_field()` method. This is needed whenever another field would have to update the slider field, because of a possible __dependency__ between these 2 fields. As an example, this is what currently happens in the __filter block__ where the selected column field updates the value field (the field type changes depending on the column type). + +```{r, update-slider-field} +ui_update.slider_field <- function(x, session, id, name) { + updateSliderInput( + session, + blockr::input_ids(x, id), + blockr::get_field_name(x, name), + blockr::value(x), + blockr::value(x, "min"), + blockr::value(x, "max"), + blockr::value(x, "step") + ) +} +``` + +## Example + +We can test our newly created field in a custom block. + +```{r, eval=TRUE, echo=FALSE} +card( + create_app_link( + "NobwRAdghgtgpmAXGKAHVA6ASmANGAYwHsIAXOMpMAdzgCMAnRRASwgGdSoAbbgCgA6YOtyIEA1gyG4ABAzioi7GQF4ZBQWAAWpUqnaIA9IZFjJAWiIMA5hmstSWgK50MLIodqNz8xe2kyQjp6Bsa+RBgM5tRQ7DAYVtZCAJTJAhDp3CyMUAwAnnymEgxpGRAQcNQA+uxZACZwDFUAZixw3HUyADzmMs1OEASk7hCCEDITMgBuPE5wqjIQTvAMLBrJuOmTMjBsC0sra3wbW5MwUAAe+8uNRyfjk5wK14frmw8TGF-JMiCnRZJmBVqq12nUxtsZtw5gsoXN3ttduM1EiEWdLgtzhc0RMnqgFnicTIvhgiQRuLFlGohLUWA0mqCOkJTqUAL7pdJOFhVNioJykDC0+ktNodbq9fqDYYkPjYmR02TQeA-P4PdhaNh5ZhCxoASQgfNIEMmAKYrAN-J5dXYstkdPu2yVcCJcPmalNzFdtsCYFdKSJSIWHsQXrlQiR-tOEyxQdExU9szg3vDl0jHxkeNjZjNodkNPIqBSLPS7LKULpUHIIrBgvqjWrYp6fQGQxGspVpyh4umifbp0DTdzPojYFK0Yxg97YbAWKLaoL3aH+YUc87PArVYOdEafChpVOLGaMj43Ao1kcfCRqV+UZ79UrcCqW53qOHbDnE1LB6PJ7PF6x16qpC651A+T7LNuDCXpceYzqmo6nF+DyHsep4QOeWh8HigG3uWoGbhBO6Ej6eIfjISETAAcnAFykAAsnAjhEOCbIcks3JOKg+FwLWdL1oynRNpKrYynK7BwOw7AjHadSKrAcAdg8nHcQAynWDD6oaxq4hJUkkESwa8padI2nK9pEk6BlxoCIZTg6JrWTmU6wSO9kTMGS5wRcabbB5zkkQWZGsWU8j2E8DAqQAzPATHgkIXI8hapABDS6kNnUAQJUZAo6gyop1KUoUsOFUUxVozGaAlykPilYC5elmUcVxD68cKAn7uUlQ1FkBCPgC3bCdKozNAwRAwAsAAMsgkoppwQFUo3UMoTYCAIfD4VAPwQIt62VptbETAJy29FknDaXIRBLfsXX1QJ50TK6CwjWNRLRnsahTbe45XGo82Lewr0ZguagAIyA8MpCngsQgqe0cBDBdS3MumY4yB1EzAlUALnUdT35QDt40agDALAAjk4RDkOtqDcHkZq0r1WFwKTVRoXwGB8P9qSpBZ8nQ2Aal8STDPzAC0i3iSZIUpJCwaKlax9dZtWkAwUAcM0VgwFjSsIQ8wXpOJDBTI+nBQBIxqY6b5u3pjG3iaQ2tmJoLCrP4o4WTdPWK07ACsqOlMkYCsgAukAA", # nolint + mode = "editor" + ), + full_screen = TRUE +) +``` + +

+ +Toggle code + +

+ +
+ +```r +new_slice_block <- function(from = 0, ...) { + + n_rows <- \(data) nrow(data) + + fields <- list( + rows = new_slider_field( + value = from, + min = 0, + max = n_rows, + step = 1, + title = "Select rows" + ) + ) + + new_block( + fields = fields, + expr = quote(dplyr::slice(seq_len(.(rows)))), + name = "Slider slice block", + ..., + class = c("slice_block", "transform_block") + ) +} + +serve_stack( + new_stack( + new_dataset_block("iris"), + new_slice_block(5) + ) +) +``` +
diff --git a/vignettes/registry.Rmd b/vignettes/registry.Rmd index 5391e349..bb2bd693 100644 --- a/vignettes/registry.Rmd +++ b/vignettes/registry.Rmd @@ -17,6 +17,7 @@ knitr::opts_chunk$set( ```{r setup, echo=FALSE, message=FALSE} library(blockr) library(bslib) +library(DiagrammeR) note_box <- function(..., color) { htmltools::div( @@ -48,6 +49,46 @@ metadata: - Returned __output__. - ... +In other words, the __registry__ is a __"supermarket"__ for data analysis. As shown below, if you develop your own blocks package and registers blocks on load, these blocks become available to the end user. Therefore this makes it powerful for __collaboration__ between data science teams. + +```{r, echo=FALSE} + +mermaid(" + flowchart LR + subgraph blockr_ggplot2[blockr.ggplot2] + new_block1[New block] + new_block2[New block] + end + subgraph blockr_echarts4r[blockr.echarts4r] + new_block3[New block] + new_block4[New block] + end + blockr_ggplot2 --> |register| registry + blockr_echarts4r --> |register| registry + subgraph registry[Registry] + subgraph select_reg[Select block] + reg_name[Name: select block] + reg_descr[Description: select columns in a table] + reg_classes[Classes: select_block, tranform_block] + reg_input[Input: data.frame] + reg_output[Output: data.frame] + reg_package[Package: blockr] + end + subgraph filter_reg[Filter block] + end + filter_reg --x |unregister| trash['fa:fa-trash'] + end + ", + height = "600px" +) |> + htmlwidgets::onRender( + "function(el, x) { + el.classList.add('text-center') + } + " + ) +``` + ## Previewing available blocks Upon loading, `{blockr}` __registers__ its internal __blocks__ with `register_blockr_blocks()`.