+ - 0:00:00
Notes for current slide
Notes for next slide

Unleash Shiny

R/Pharma - Advanced Shiny Workshop

David Granjon (Novartis) & John Coene (Opifex)

10-10-2021

1 / 65

About us

David

Data Scientist at Novartis

@divadnojnarg

John

Software Engineer at Opifex

@jdatap

2 / 65

Program

We're in for 2 hours of fun!

  • Grab a ☕
  • Make yourself comfortable 🛋 or 🧘
  • Ask questions ❓
  1. Introduction 5 min
  2. Bundling with packer 15 min
  3. Project Setup 10 min
  4. Break 5 min
  5. Framework 7 60 min
  6. Break 5 min
  7. R model 10 min
  8. Echarts.js (Homework)
  9. Questions
3 / 65

Workshop Material

Clone this repository with the RStudio IDE or via the command line.

git clone https://github.com/RinteRface/unleash-shiny-2021.git
cd unleash-shiny-2021

Then run renv::restore() to install the dependencies.

Importantly, this workshop makes heavy use of npm (Node's Package Manager), it comes with the installation of node.js:

4 / 65

Introduction

5 / 65

Intro

  • Developing custom design for Shiny takes too much time ...
  • {shinyMobile} development >= 2 years ...
  • What if you are already know a bit of HTML/CSS/JS?
  • Relies on existing web templates to save time.
  • Focus on app features.

6 / 65

Workshop objectives

  • Setup rigorous R package structure for your app with {golem}.
  • Learn how to use modern web stack tools like webpack to maintain your R/JS package with {packer}.
  • Learn how R (server) and JS (client) communicate to exchange data.
  • Learn modular JavaScript basics.
  • Learn basics of JSX.
  • Design awesome user interface with external template.

7 / 65

Managing JavaScript

8 / 65

Pre-processing JavaScript

Why?

What?

9 / 65

Code Management

Large projects are complex, the developer needs help

Use something that enforces good practice and project structure.

In R

In JavaScript

There is nothing such---built-in---we work with loose collections of files.

10 / 65

Browser Support

✅ R code written in 4.0.0 will (likely) run on 3.0.0+

💀 JavaScript not so much.

[caniuse.com](https://caniuse.com/?search=ES6)

ES6 released June 2016

11 / 65

Code Size

Code size matters in JavaScript: the smaller the file the faster it loads.

Input

Can be written and read by a human.

function addOne(xyz){
return xyz + 1;
}
addOne(2);

Minified

Loads faster but can't be written or read by a human.

function addOne(n){return n+1}addOne(2);
12 / 65

Conclusion

We need to pre-process the code to:

  • Minify it for performances
  • Transpile to ensure it runs an (nearly) all browsers
  • Be able to setup code management

It does not end here.

Pre-processing enables even more.

13 / 65

Dependency Management

No packages system with JavaScript but there is one for node.js---Node's Package Manager (NPM)---that can be used when preprocessing the code.

MANUAL

<script src="file1.js"></script>
<script src="file2.js"></script>
<script src="file3.js"></script>

👎 Difficult to manage.

NPM

npm install dplyr

then

import { mutate } from 'dplyr';

👍 Easy to manage.

14 / 65

Tree-shaking

Checking for dead variables.

Code checks

Discover errors when you write the code, not when you run it.

And so much more...

15 / 65

Downside

  1. There are a variety of tools to do the job (webpack, Grunt, Parcel, etc.)
  2. They are generally difficult to set up.
  3. They're not designed to work with R in mind.
16 / 65

Meet packer!

packer.john-coene.com

17 / 65

Principles

Anything packer-related takes places in a 📦

Does not become a dependency to what you're building

It aspires to be a specialised {usethis}:

automate tasks when building packages with JavaScript.

18 / 65

Using packer

Set it up first.

  1. Create a package (or {golem} app)
  2. Scaffold

Then write some code and bundle with packer::bundle(), which produces the JavaScript code.

19 / 65

Scaffolds?

Scaffolds are central to packer. They create the necessary structure to use webpack and npm with R.

  • Golem - use packer with {golem}
  • Htmlwidgets - use packer to create {htmlwidgets}
  • Extensions - create shiny extensions (handlers)
  • Inputs - create custom inputs
  • Outputs - create custom outputs
  • And a few more.

e.g.:

packer::scaffold_golem()

The term "scaffold" was blatantly stolen from htmlwidgets::scaffoldWidget.

20 / 65

Bundle?

Source code is written in ./srcjs and is bundled to ./inst

packer::bundle()
21 / 65

Project Setup

22 / 65

Intro

  • Develop a simple app utilizing a cutting edge web template (Framework7).
  • This app will consist in:
    • A JS powered select input controlling a variable.
    • A visualization powered by {ggplot2}
    • Homework: replace {ggplot2} by JS driven visualization.

23 / 65

Scaffold Framework7 🚀🚀🚀

We call:

packer::scaffold_golem(framework7 = TRUE)

Sets a Framework7 compatible structure for {golem}:

  • Install npm dependencies.
  • JS assets folder ./srcjs.
  • Loaders for CSS, JS, JSX ...
  • Config for webpack.
  • So that you don't have to worry too much.

24 / 65

Test it 🥼

This is a very basic app but it works well.

packer::bundle()
pkgload::load_all()
run_app()

Bad news ... it is time for the ...

25 / 65

Break!

See you in 5 minutes.

26 / 65

Framework 7

27 / 65

Intro


  • First class mobile template for the web.
  • Native look and feel for iOS and Android
  • Progressive web app (PWA) support.
  • ... also works for desktop apps 😏

28 / 65
29 / 65

Import Framework7 and CSS 👩‍🏫

Inside the main ./srcjs/index.js.

  • Notice how CSS is imported.
  • Requires style/css loaders.
  • Modular approach: import only what is required.
  • Lighter JS bundle.
  • Faster app.
// Import Framework7
import Framework7 from 'framework7';
// Import Framework7 Styles
import 'framework7/framework7-bundle.min.css';
30 / 65

Layout basics 👩‍🏫

This part is handled by app_ui.R.

Framework7 requires an index.html script:

  • <div id="app"></div> is the app root required for initialization.
  • index.js the script generated with {packer} after bundling.
  • golem_add_external_resources() fills the <!-- Head content ... --> .
<!DOCTYPE html>
<html>
<head>
<!-- Head content ... -->
</head>
<body>
<!-- App root element ... -->
<div id="app"></div>
<!-- Path to Framework7 JS-->
<script type="text/javascript" src="www/index.js"></script>
</body>
</html>
31 / 65

App template 👩‍🏫

The simplest Framework7 layout is composed of:

  • The app wrapper with unique id required for initialization.
  • A single auto-initialized view.
  • A page with:
    • navbar (top).
    • toolbar (bottom).
    • page content (middle).
<div id="app">
<div class="view view-main view-init safe-areas">
<div class="page">
<!-- navbar -->
<!-- toolbar -->
<div class="page-content"></div>
</div>
</div>
</div>
32 / 65

HTML + JS: welcome JSX 👩‍🏫

Combine HTML and JavaScript code in the same file.

Without JSX

<template>
<div class="title" innerHTML=${title}></div>
</template>
<script>
export default (props) => {
const title = 'Hello World';
return $render;
}
</script>

👎 Difficult to manage.

With JSX

export default () => {
const title = 'Hello World';
return () => (
<div class="title">{title}</div>
)
}

👍 easy to manage.

33 / 65

JSX in RStudio ⚠️

  • RStudio is not a web development oriented IDE.
  • JSX appears as text file.
  • You may change it to JavaScript.

34 / 65

About Framework7 components 👩‍🏫

  • Components have 2 default parameters:
    • props gather all passed attributes.
    • context provides access to:
    • The app instance $f7.
    • More here.
  • Components:
    • must return a render function.
    • can be self-closed.
<Component user="David" id="compo"/>
const Component = (props, context) {
const greetings = 'Hello ' + props.user;
// render function
return () => (
<h1>{greetings}</h1>
)
}
35 / 65

Main app component 👩‍🏫

{packer} created app.f7.jsx.

export default (props, { $f7 }) => {
const title = 'Hello World';
return () => (
<div id="app">
<div class="view view-main view-init safe-areas">
<div class="page">
<!-- navbar ... -->
<!-- toolbar ... -->
<div class="page-content">
<div class="block strong">
Page Content
</div>
</div>
</div>
</div>
</div>
)
}
36 / 65

Initialize App instance 👩‍🏫

The remaining of ./srcjs/index.js:

  • Initializes the app instance:
    • Targets app id.
    • Sets custom theme.
    • Plugs the App component in the component slot.
    • Many other options available ...
// ... Other imports ...
import App from './components/app.f7.jsx';
let app = new Framework7({
el: '#app',
theme: 'ios',
// specify main app component
component: App
});
37 / 65

Customize app: your turn 🥼

  • Within let app = new Framework7({ ... }):

    • Change the theme to md (see doc).
  • In ./R/app_ui.R:

    • Add theme-dark class to the body tag.
    • Add color-theme-***, where *** is a color from here.
38 / 65

About the Framework7 smartSelect 👩‍🏫

It's time to add an extra component.

  • Improved selectInput().
  • Triggered by a element.
  • select contains multiple option tags.
  • select is followed by a label tag.
<div class="list">
<ul>
<!-- Smart select item -->
<li>
<!-- Additional "smart-select" class -->
<a href="#" class="item-link smart-select">
<!-- select -->
<select name="fruits">
<option value="apple" selected>Apple</option>
...
</select>
<!-- Select label -->
</a>
</li>
</ul>
</div>
39 / 65

Widget element (1/6): your turn 🥼

  • Open ./srcjs/index.js.
  • Between CSS import and the app initialization, add the following code.
  • We also import Sheet since smartSelect needs it to open.
// ./srcjs/index.js
// Install F7 Components using .use() method on class:
import Sheet from 'framework7/esm/components/sheet/sheet.js';
import smartSelect from 'framework7/esm/components/smart-select/smart-select.js';
Framework7.use([Sheet, smartSelect]);
40 / 65

Widget element (2/6): your turn 🥼

  • Create a new ./srcjs/components/widget.f7.jsx component.
  • Copy the preliminary code.
  • Replace ID and LABEL by the relevant props elements.
  • Don't change data-open-in="sheet".
// ./srcjs/components/widget.f7.jsx
export default (props, { $f7 }) => {
return () => (
<div class="list">
<ul>
<li>
<a
class="item-link smart-select smart-select-init"
id=ID
data-open-in="sheet">
<select name=ID>
</select>
<div class="item-content">
<div class="item-inner">
<div class="item-title">LABEL</div>
</div>
</div>
</a>
</li>
</ul>
</div>
)
}
41 / 65

Widget element (3/6): your turn 🥼

  • Import the widget in the main app component.
  • Don't forget that widget.f7.jsx and app.f7.jsx are in the same folder.
  • Fill in the ___.
// ./srcjs/components/app.f7.jsx
import ___ from '___';
export default (props, { $f7 }) => {
// code omitted ...
return () => (
<div id="app">
...
</div>
)
}
42 / 65

Widget element (4/6): your turn 🥼

Let's add some <option> to the <select> tag...

  1. Create select options:
    • Create a names array containing wt, hp and qsec strings.
    • Map over each array element to build an <option> array.
  2. Update render function:
    • Add the newly created elements to <select>.

Fill in the ___.

// ./srcjs/components/widget.f7.jsx
export default (props, { $f7 }) => {
// (1)
const names = [___];
const selectOptions = ___.map(
(name) => {
let isSelected = name === 'wt'? true: false;
return(
<option
key={___}
value={___}
selected={___}>
{___}
</option>
);
}
)
// (2)
return () => (
// other tags are not shown
<select name=ID>
{___}
</select>
)
}
43 / 65

Widget element (5/6): your turn 🥼

Let's send the selected value to Shiny (1/2).

  1. Create getSelectValue:
    • Give it an id parameter.
    • Smart select instance is recovered with $f7.smartSelect.get() method.
    • Smart select value obtained with instance.getValue().
    • Call the relevant Shiny JS method to set the input value.
// ./srcjs/components/widget.f7.jsx
export default (props, { $f7 }) => {
// ... code from previous step ...
// Recover select value
const getSelectValue = (id) => {
let select = $f7.smartSelect.get('#' + ___);
Shiny.___(___, select.getValue());
};
// render function
return () => (...)
}
44 / 65

Widget element (6/6): your turn 🥼

Let's send the selected value to Shiny (2/2).

  1. In the component render function:
    • Add an onChange prop to the <a> element.
    • onChange triggered each time a new value is selected.
    • Inside onChange, call getSelectValue with relevant ID parameter.
    • Sends it to Shiny with Shiny.setInputValue.
// ./srcjs/components/widget.f7.jsx
export default (props, { $f7 }) => {
// ... code from previous step ...
// render function
return () => (
// Other tags are not shown
<a
class="item-link smart-select smart-select-init"
onChange={() => ___(ID)}
id=ID
data-open-in="sheet">
... // ommitted since unchanged
</a>
)
}
45 / 65

Test it 🥼

  • Open ./R/app_server.R.
  • Add this code, replacing ID by what you chose JS side.
  • Input does not have initial value...
# Inside ./R/app_server.R
observeEvent(input$<ID>, {
message(sprintf("Slider value: %s", input$<ID>))
})
packer::bundle()
pkgload::load_all()
run_app()
46 / 65

Test it 🥼

packer::bundle()
pkgload::load_all()
run_app()

🏆 Congrats! You designed your first component. Let's improve it by adding some R logic.

47 / 65

Break!

See you in 5 minutes.

48 / 65

R Model

49 / 65

Intro

  • Simple regression model with the mtcars dataset and {ggplot2}.
  • One variable is selected from JS with newly designed custom widget.
  • Formula lm(mpg ~ input$var, mtcars).
50 / 65

R business logic: your turn 🥼

  • Open ./R/app_server.R.
  • Fill in the ___ and ....
output$plot <- renderPlot({
ggplot(
data = ___,
mapping = aes(x = mpg, y = .data[[___]])
) +
geom_...() +
geom_smooth()
})

That's all folks!

51 / 65

Insert the plot 🥼

  • output$plot works by pair with renderPlot("plot") ...
  • but ... we can't insert the plot as R code inside app_ui.R.
  • Don't forget that Shiny is just creating HTML from R.

In the R console, run:

plotOutput("plot")

which yields:

<div id="id" class="shiny-plot-output" style="width:100%;height:400px;"></div>

Insert this in ./srcjs/components/app.f7.jsx.

52 / 65

Improve the design 🥼

  • Explore the Framework7 card container documentation.
  • Include the previous plot in the container of your choice.
  • Recompile the JS code and run the app.

53 / 65

Homework: Echarts.js

54 / 65

Intro

  • Replace the previous {ggplot2} chart by JS code.
  • We'll have to send data from R to JS.

Communication done through the websocket.

  • R side:

    • sendCustomMessage sends R messages to JS.
  • JS side:

    • Shiny.addCustomMessageHandler receives message from session$sendCustomMessage. Both are linked by the type parameter

55 / 65

echarts assets

  • Install echarts and echarts-gl.
  • Import JS assets in ./srcjs/components/widget.f7.jsx.
  • Review the echarts documentation.
packer::npm_install(
c(
"echarts",
"echarts-gl"
),
scope = "prod"
)
// Import plotting library
import * as echarts from 'echarts';
import 'echarts-gl';
56 / 65

echarts plotting strategy

  • Create a DOM container for the plot.
  • Initialize the plot container.
  • Set plot options.
  • Update plot instance.
<div id="plot" style="width:100%; min-height:400px;"></div>
let chart = echarts.init(document.getElementById('plot'));
myChartOptions = {
title: { text: 'Plot title' },
legend: { data:[...] },
xAxis: { data: ...},
yAxis: { type: ... },
series: [
{
name: ...,
type: ...,
data: ...
},
// other data
]
};
myChart.setOption(myChartOptions);
57 / 65

Process R data

  • Create a new ./R/process.R script.
  • process_data() selects the relevant column based on a given parameter.
  • Fill in the ___.
process_data <- function(parm, session = shiny::getDefaultReactiveDomain()) {
data_subset <- list(mtcars$mpg, mtcars[[___]])
names(data_subset) <- c("mpg", parm)
processed_data <- list(
data = ___,
var = ___
)
session$sendCustomMessage("model_data", ___)
}
58 / 65

Add Shiny

  • In ./R/app_server.R, add an observeEvent() to trigger process_data() based on the select input value (JS side).
  • Comment out the old {ggplot2} code.
  • In ./srcjs/components/app.f7.jx, remove the shiny-plot-output class from the plot container.
observeEvent(___, {
___(___)
})
59 / 65

Recover R data (1/4)

  • Inside ./srcjs/components/widget.f7.jsx create a new renderPlot function, right after the widget component.
// ./srcjs/components/widget.f7.jsx
export default (props, { $f7 }) => {
// ...
renderPlot();
return () => (...)
}
const renderPlot = () => {
// JS logic
}
60 / 65

Recover R data (2/4)

  • Inside renderPlot function:
    • Add shiny:connected event listener.
    • Initialize the echarts plot instance.
    • Copy and paste the resize event to handle plot resize.
// ./srcjs/components/widget.f7.jsx
let plot;
$(document).on('shiny:connected', () => {
// prepare echarts plot
plot = echarts.___(document.getElementById(___));
});
// Resize event
$(window).on('resize', function() {
plot.resize();
});
61 / 65

Recover R data (3/4)

  • Add Shiny.addCustomMessageHandler to capture the message sent from R.
  • A R list translates into a JS object.
  • Fill in the ___.
//./srcjs/components/widget.f7.jsx
let plotOptions, p, data;
Shiny.addCustomMessageHandler(___, (___) => {
p = message.var;
data = message.___;
});
62 / 65

Recover R data (4/4)

  • In the same message handler, set plot options.
  • Apply options to the plot instance.
  • Fill in the ___.
// ./srcjs/components/widget.f7.jsx
plotOptions = {
title: { text: 'Plot' },
tooltip: {},
legend: { data:['mpg', p] },
xAxis: { data: message.data[___] },
yAxis: { type: 'value' },
series: [
{
name: p,
type: 'scatter',
data: message.data[___]
}
]
}
___.setOption(___);
63 / 65

Questions?

64 / 65
65 / 65

About us

David

Data Scientist at Novartis

@divadnojnarg

John

Software Engineer at Opifex

@jdatap

2 / 65
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
oTile View: Overview of Slides
Alt + fFit Slides to Screen
Esc Back to slideshow