Slide 1

Slide 1 text

Engineering {shiny} with {golem} EARL 2020 2020-10-15 Colin Fay - ThinkR Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 1 / 84

Slide 2

Slide 2 text

Today's menu Program Introduction Understanding {golem} Developing a {golem} app Testing & Sending to prod Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 2 / 84

Slide 3

Slide 3 text

INTRODUCTION Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 3 / 84

Slide 4

Slide 4 text

$ whoami Colin FAY Data Scientist & R-Hacker at ThinkR, a french company focused on Data Science & R. Hyperactive open source developer, lead developer of the {golem} project. https://thinkr.fr https://rtask.thinkr.fr https://twitter.com/_colinfay https://github.com/colinfay https://colinfay.me Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 4 / 84

Slide 5

Slide 5 text

ThinkR Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 5 / 84

Slide 6

Slide 6 text

Data Science engineering, focused on R.  Training  Software Engineering  R in production  Consulting ThinkR Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 6 / 84

Slide 7

Slide 7 text

Logistics https://speakerdeck.com/colinfay https://connect.thinkr.fr/make-a-golem @_ColinFay @earlconf Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 7 / 84

Slide 8

Slide 8 text

ABOUT GOLEM Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 8 / 84

Slide 9

Slide 9 text

{golem} is an R package that contains a framework for building production-ready Shiny Applications. golem <- cranlogs::cran_downloads( "golem", from = "2019-08-01" ) sum(golem$count) [1] 40295 {golem} Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 9 / 84

Slide 10

Slide 10 text

Why using {golem}?  Automate the boring stuff repetitive tasks  Work with reliable tools  Gain time developing  Simplify deployment  Standardize team work About {golem} at ThinkR:  Internal need, used on a daily basis  Need reliable tooling for deploying to our clients' environments  Build and share good practices globally  Promote R & Shiny in production Why {golem}? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 10 / 84

Slide 11

Slide 11 text

⌨ Answer in the chat ⌨ What makes a production-grade {shiny} app? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 11 / 84

Slide 12

Slide 12 text

{golem} central philosophy Shiny App As a Package What's a "prod-ready" Shiny App? Has meta data (DESCRIPTION) Divided in functions (R/) Tested (tests/) With dependencies (NAMESPACE) Documented (man/ & vignettes) Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 12 / 84

Slide 13

Slide 13 text

{golem} central philosophy Shiny App As a Package The plus side: everything you know about package development works with {golem}. Notably:  Documentation  Testing Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 13 / 84

Slide 14

Slide 14 text

Understanding {golem} Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 14 / 84

Slide 15

Slide 15 text

⚠ Warning ⚠ packageVersion("golem") [1] '0.3.0' remotes::install_github( "thinkr-open/golem" ) https://connect.thinkr.fr/make-a-golem / Ex 1 02:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 15 / 84

Slide 16

Slide 16 text

Create a {golem} https://connect.thinkr.fr/make-a-golem / Ex 2 02:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 16 / 84

Slide 17

Slide 17 text

Understanding {golem} fs::dir_tree("golex") golex ├── DESCRIPTION ├── NAMESPACE ├── R │ ├── app_config.R │ ├── app_server.R │ ├── app_ui.R │ └── run_app.R ├── dev │ ├── 01_start.R │ ├── 02_dev.R │ ├── 03_deploy.R │ └── run_dev.R ├── inst │ ├── app │ │ └── www │ │ └── favicon.ico │ └── golem-config.yml └── man └── run_app.Rd Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 17 / 84

Slide 18

Slide 18 text

Understanding {golem} Standard package files (i.e. not {golem}-specific): DESCRIPTION: meta-data NAMESPACE: exported functions + functions from other R/: functions (everything in {golem} is a function) man/: documentation Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 18 / 84

Slide 19

Slide 19 text

app_server.R #' The application server-side #' #' @param input,output,session Internal parameters for {shiny}. #' DO NOT REMOVE. #' @import shiny #' @noRd app_server <- function( input, output, session ) { # Your application server logic } server logic can be thought of as a drop in replacement of server.R series of callModule() / module_server() (if used) Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 19 / 84

Slide 20

Slide 20 text

app_ui.R #' @param request Internal parameter for `{shiny}`. #' DO NOT REMOVE. #' @import shiny #' @noRd app_ui <- function(request) { tagList( # Leave this function for adding external resources golem_add_external_resources(), # Your application UI logic fluidPage( h1("golex") ) ) } UI counterpart put UI content after the # Your application UI logic line Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 20 / 84

Slide 21

Slide 21 text

app_ui.R golem_add_external_resources <- function(){ add_resource_path( 'www', app_sys('app/www') ) tags$head( favicon(), bundle_resources( path = app_sys('app/www'), app_title = 'golex' ) # Add here other external resources # for example, you can add shinyalert::useShinyalert() ) } Used to add external resources Will integrate CSS and JS in the app Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 21 / 84

Slide 22

Slide 22 text

app_config.R #' Access files in the current app #' #' NOTE: If you manually change your package name in the DESCRIPTION, #' don't forget to change it here too, and in the config file. #' For a safer name change mechanism, use the `golem::set_golem_name()` function. #' #' @param ... character vectors, specifying subdirectory and file(s) #' within your package. The default, none, returns the root of the app. #' #' @noRd app_sys <- function(...){ system.file(..., package = "golex") } app_sys("x") will refer to inst/x Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 22 / 84

Slide 23

Slide 23 text

app_config.R get_golem_config <- function( value, config = Sys.getenv("R_CONFIG_ACTIVE", "default"), use_parent = TRUE ){ config::get( value = value, config = config, # Modify this if your config file is somewhere else: file = app_sys("golem-config.yml"), use_parent = use_parent ) } Retrieves elements from inst/golem-config.yml Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 23 / 84

Slide 24

Slide 24 text

golem-config.yml golem_version: 0.0.0.9000 app_prod: no production: app_prod: yes dev: Uses the {config} format Can be safely ignored if you don't feel like you need it Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 24 / 84

Slide 25

Slide 25 text

run_app.R #' @param ... arguments to pass to golem_opts #' @inheritParams shiny::shinyApp #' #' @export #' @importFrom shiny shinyApp #' @importFrom golem with_golem_options run_app <- function( onStart = NULL, options = list(), enableBookmarking = NULL, ... ) { with_golem_options( app = shinyApp( ui = app_ui, server = app_server, onStart = onStart, options = options, enableBookmarking = enableBookmarking ), golem_opts = list(...) ) } Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 25 / 84

Slide 26

Slide 26 text

run_app.R About with_golem_options Allows to pass arguments to run_app() will later be callable with golem::get_golem_options() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 26 / 84

Slide 27

Slide 27 text

Understanding {golem} inst/app/www/ Host external files, notably the one created with: golem::add_css_file() golem::add_js_file() golem::add_js_handler() golem::add_js_input_binding() golem::add_js_output_binding() golem::use_favicon() golem::add_html_template() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 27 / 84

Slide 28

Slide 28 text

About the dev/ folder Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 28 / 84

Slide 29

Slide 29 text

About the dev/ folder fs::dir_tree("golex/dev") golex/dev ├── 01_start.R ├── 02_dev.R ├── 03_deploy.R └── run_dev.R Four files that bundle the golem workflow: 01_start.R: run once at the beginning of the project 02_dev.R: day to day development 03_deploy.R: to use before sending to prod run_dev.R: to relaunch your app during development Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 29 / 84

Slide 30

Slide 30 text

01_start.R Fill the DESCRIPTION file https://connect.thinkr.fr/make-a-golem / Ex 3 05:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 30 / 84

Slide 31

Slide 31 text

01_start.R To be launched for setting elements: golem::set_golem_options() Fills the yaml file golem::use_recommended_tests() and golem::use_recommended_deps() Sets common dependencies and tests Use golem::set_golem_name() to globally change your app name Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 31 / 84

Slide 32

Slide 32 text

01_start.R {usethis} commonly used calls usethis::use_mit_license( name = "Golem User" ) # You can set another license here usethis::use_readme_rmd( open = FALSE ) usethis::use_code_of_conduct() usethis::use_lifecycle_badge( "Experimental" ) usethis::use_news_md( open = FALSE ) usethis::use_git() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 32 / 84

Slide 33

Slide 33 text

01_start.R golem::use_recommended_deps() golem::use_favicon() golem::use_utils_ui() & golem::use_utils_server() https://connect.thinkr.fr/make-a-golem / Ex 4 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 33 / 84

Slide 34

Slide 34 text

DEV Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 34 / 84

Slide 35

Slide 35 text

STRUCTURING YOUR APPLICATION Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 35 / 84

Slide 36

Slide 36 text

ABOUT DATA Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 36 / 84

Slide 37

Slide 37 text

⌨ Answer in the chat ⌨ What is the usual source of your data when building an app? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 37 / 84

Slide 38

Slide 38 text

Adding datasets You might need internal data inside your app Call usethis::use_data_raw from dev/02_dev.R ## Add internal datasets ---- ## If you have data in your package usethis::use_data_raw( name = "dataset" ) Creates data-raw/dataset.R Inside this file: ## code to prepare `dataset` dataset goes here usethis::use_data("dataset") Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 38 / 84

Slide 39

Slide 39 text

Adding datasets ## code to prepare `dataset` dataset goes here library(tidyverse) my_app_dataset <- read.csv("bla/bla/bla.csv") my_app_dataset <- my_app_dataset %>% filter(this == "that") %>% arrange(on_that) %>% select( -contains("this") ) usethis::use_data(my_app_dataset) Now available as my_app_dataset inside your app. Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 39 / 84

Slide 40

Slide 40 text

External data (database) If your app is large or changes frequently, it's better to use a database as a backend. Choice Update Size Package data Never to very rare Low to medium Reading files Uploaded by Users Preferably low External DataBase Never to Streaming Low to Big Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 40 / 84

Slide 41

Slide 41 text

Adding data in your app https://connect.thinkr.fr/make-a-golem / Ex 5 05:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 41 / 84

Slide 42

Slide 42 text

Application logic vs business logic Application logic: the things that make your Shiny app interactive Business logic: core algorithms and functions that make your application specific to your area of work Keep them separated! Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 42 / 84

Slide 43

Slide 43 text

Structuring your app Naming convention: app_*: global infrastructure functions fct_*: business logic functions mod_*: file with ONE module (ui + server) utils_*: cross module utilitarian functions *_ui_* / *_server_*: relates to UI and SERVER Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 43 / 84

Slide 44

Slide 44 text

: R/connect.R : R/summary.R : R/plot.R : R/odbc.R Why bother? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 44 / 84

Slide 45

Slide 45 text

: R/connect.R : R/summary.R : R/plot.R : R/odbc.R : R/fct_connect.R : R/mod_summary.R : R/utils_plot.R : R/fct_odbc.R Why bother? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 45 / 84

Slide 46

Slide 46 text

02_dev.R golem::add_fct( "helpers" ) golem::add_utils( "data_ui" ) ✓ File created at R/fct_helpers.R ● Go to R/fct_helpers.R ✓ File created at R/utils_data_ui.R ● Go to R/utils_data_ui.R Creates fct_* and utils_* files golem::add_module( name = "my_first_module") ✓ File created at R/mod_my_first_module.R ● Go to R/mod_my_first_module.R Builds a skeleton for a Shiny Module Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 46 / 84

Slide 47

Slide 47 text

USING SHINY MODULES Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 47 / 84

Slide 48

Slide 48 text

⌨ Answer in the chat ⌨ Have you ever used {shiny} modules? Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 48 / 84

Slide 49

Slide 49 text

About Shiny Modules "Pieces" of Shiny Apps Always come in pair (UI + Server) Manage the unique IDs necessity of Shiny "Functionnalize" your app Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 49 / 84

Slide 50

Slide 50 text

One million “Validate” buttons library(shiny) ui <- function(request){ fluidPage( sliderInput("choice1", "choice 1", 1, 10, 5), actionButton("validate1", "Validate choice 1"), sliderInput("choice2", "choice 2", 1, 10, 5), actionButton("validate2", "Validate choice 2") ) } server <- function(input, output, session){ observeEvent( input$validate1 , { print(input$choice1) }) observeEvent( input$validate2 , { print(input$choice2) }) } shinyApp(ui, server) Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 50 / 84

Slide 51

Slide 51 text

Why Shiny Modules Functionalizing Shiny elements: name_ui <- function(id){ ns <- NS(id) tagList( sliderInput(ns("choice"), "Choice", 1, 10, 5), actionButton(ns("validate"), "Validate Choice") ) } name_server <- function(id){ moduleServer(id, function(input, output, session) { observeEvent( input$validate , { print(input$choice) }) }) } Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 51 / 84

Slide 52

Slide 52 text

Why Shiny Modules Functionalizing Shiny elements: library(shiny) ui <- function(request){ fluidPage( name_ui("name_ui_1"), name_ui("name_ui_2") ) } server <- function(input, output, session){ name_server("name_ui_1") name_server("name_ui_2") } shinyApp(ui, server) Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 52 / 84

Slide 53

Slide 53 text

Why Shiny Modules id <- "name_ui_1" ns <- NS(id) ns("choice") [1] "name_ui_1-choice" name_ui <- function(id, butname){ ns <- NS(id) tagList( actionButton(ns("validate"), butname) ) } name_ui("name_ui_1", "Validate Choice") name_ui("name_ui_2", "Validate Choice, again") Validate Choice Validate Choice, again Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 53 / 84

Slide 54

Slide 54 text

02_dev.R golem::add_module( name = "my_first_module") Builds a skeleton for a Shiny Module Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 54 / 84

Slide 55

Slide 55 text

02_dev.R #' my_first_module UI Function #' #' @description A shiny Module. #' #' @param id,input,output,session Internal parameters for {shiny}. #' #' @noRd #' #' @importFrom shiny NS tagList mod_my_first_module_ui <- function(id){ ns <- NS(id) tagList( ) } Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 55 / 84

Slide 56

Slide 56 text

02_dev.R #' my_first_module Server Functions #' #' @noRd mod_my_first_module_server <- function(id){ moduleServer( id, function(input, output, session){ ns <- session$ns }) } ## To be copied in the UI # mod_my_first_module_ui("my_first_module_ui_1") ## To be copied in the server # mod_my_first_module_server("my_first_module_ui_1") Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 56 / 84

Slide 57

Slide 57 text

⚠ DON'T FORGET THE NS() IN THE UI ⚠ Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 57 / 84

Slide 58

Slide 58 text

UNDERSTANDING NAMESPACE & DEPENDENCIES Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 58 / 84

Slide 59

Slide 59 text

About the NAMESPACE file One of the most important files of your package ⚠ NEVER EDIT BY HAND ⚠ Describes how your package interacts with R, and with other packages Lists functions that are exported (from your app) and imported (from other packages) Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 59 / 84

Slide 60

Slide 60 text

What's a dependency? Your app needs external functions (at least from {shiny}) The DESCRIPTION file contains the package dependencies Are added to the DESCRIPTION with: usethis::use_package("attempt") Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 60 / 84

Slide 61

Slide 61 text

What's a dependency? You also need to add tags on top of each functions that specify what deps are imported Either with @import (a whole package) and @importFrom (a specific function). golem built modules, by default, import elements from {shiny}: #' @importFrom shiny NS tagList Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 61 / 84

Slide 62

Slide 62 text

Do this for EACH function #' @import magrittr #' @importFrom stats na.omit mean_no_na <- function(x){ x <- x %>% na.omit() sum(x)/length(x) } You can use import or importFrom. The better is to use importFrom, for preventing namespace conflict. Add to EACH function. It will take some time, but it's better on the long run. Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 62 / 84

Slide 63

Slide 63 text

TO SUM UP "I want to use dplyr::filter in my_module.R" 1. usethis::use_package('dplyr') in the console 2. #' @importFrom dplyr filter in the file 3. devtools::document() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 63 / 84

Slide 64

Slide 64 text

Building your first module https://connect.thinkr.fr/make-a-golem / Ex 6 15:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 64 / 84

Slide 65

Slide 65 text

INCLUDING EXTERNAL FILES Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 65 / 84

Slide 66

Slide 66 text

golem_add_external_resources() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 66 / 84

Slide 67

Slide 67 text

golem_add_external_resources() In app_ui.R, the golem_add_external_resources() functions add to the app every .css and .js file contained in inst/app/www golem_add_external_resources <- function(){ add_resource_path( 'www', app_sys('app/www') ) tags$head( favicon(), bundle_resources( path = app_sys('app/www'), app_title = 'golex' ) # Add here other external resources # for example, you can add shinyalert::useShinyalert() ) } Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 67 / 84

Slide 68

Slide 68 text

Use external resources golem::add_css_file() golem::add_js_file() golem::add_js_handler() golem::add_js_input_binding() golem::add_js_output_binding() golem::use_favicon() golem::add_html_template() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 68 / 84

Slide 69

Slide 69 text

Add CSS to your app https://connect.thinkr.fr/make-a-golem / Ex 7 05:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 69 / 84

Slide 70

Slide 70 text

TEST AND SEND Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 70 / 84

Slide 71

Slide 71 text

Using unit tests Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 71 / 84

Slide 72

Slide 72 text

What do we test Focus on business logic testing: it's more important to test that the algorithm is still accurate than testing the UI As we've separated businness logic from application logic, we use package development testing tools We'll use the {testthat} Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 72 / 84

Slide 73

Slide 73 text

01_start.R ## Init Testing Infrastructure ---- ## Create a template for tests golem::use_recommended_tests() Recommended tests 02_dev.R ## Tests ---- ## Add one line by test you want to create usethis::use_test( "app" ) Add custom test Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 73 / 84

Slide 74

Slide 74 text

What the users see What the users interact with General front-end/design What - User Interface Your time is limited, so if you have to choose don't focus too much on testint the UI only Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 74 / 84

Slide 75

Slide 75 text

Core algorithms that make your app "unique" Business knowledge What your users rely on What - Business logic Try to test business logic as extensively as possible Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 75 / 84

Slide 76

Slide 76 text

How much CPU & RAM does your application need Bad estimate will lead to slow application performances If the app needs to scale, it's crucial to know it upfront What - Application Load Poor app performances lead to bad UX, and potentially cost Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 76 / 84

Slide 77

Slide 77 text

-> Leverage standard testing frameworks test_that("The meaning of life is 42", { expect_equal( meaning_of_life(), 42 ) }) Shiny App as a package Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 77 / 84

Slide 78

Slide 78 text

UI regressions {shinytests} Test visual regression of your application puppeteer Command line tool to mock a web session, in NodeJS {crrry} R tool to drive a {shiny} session Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 78 / 84

Slide 79

Slide 79 text

Testing the app load {shinyloadtest} : native R package + Cli to record and replay load tests {dockerstats} : get Docker stats inside R {crrry} + {dockerstats} : replay session and watch the Docker stats Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 79 / 84

Slide 80

Slide 80 text

SEND TO PRODUCTION Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 80 / 84

Slide 81

Slide 81 text

## RStudio ---- ## If you want to deploy on RStudio related platforms golem::add_rstudioconnect_file() golem::add_shinyappsio_file() golem::add_shinyserver_file() ## Docker ---- ## If you want to deploy via a generic Dockerfile golem::add_dockerfile() ## If you want to deploy to ShinyProxy golem::add_dockerfile_shinyproxy() ## If you want to deploy to Heroku golem::add_dockerfile_heroku() Send to prod Once everything is tested, you can send to prod using {golem} Deploy on a server, or build as a tar.gz with pkgbuild::build() Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 81 / 84

Slide 82

Slide 82 text

Deploy https://connect.thinkr.fr/make-a-golem / Ex 8 05:00 Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 82 / 84

Slide 83

Slide 83 text

engineering-shiny.org Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 83 / 84

Slide 84

Slide 84 text

Thx! Questions? Colin Fay Online [email protected] http://twitter.com/_colinfay http://twitter.com/thinkr_fr https://github.com/ColinFay https://thinkr.fr/ https://rtask.thinkr.fr/ https://colinfay.me/ Colin FAY (@_ColinFay) - https://rtask.thinkr.fr 84 / 84