David
Data Scientist at Novartis
@divadnojnarg
John
Software Engineer at Opifex
@jdatap
We're in for 2 hours of fun!
Clone this repository with the RStudio IDE or via the command line.
git clone https://github.com/RinteRface/unleash-shiny-2021.gitcd 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:
{shinyMobile}
development >=
2 years ...✅ R code written in 4.0.0
will (likely) run on 3.0.0
+
💀 JavaScript not so much.
Code size matters in JavaScript: the smaller the file the faster it loads.
Can be written and read by a human.
function addOne(xyz){ return xyz + 1;}addOne(2);
Loads faster but can't be written or read by a human.
function addOne(n){return n+1}addOne(2);
We need to pre-process the code to:
It does not end here.
Pre-processing enables even more.
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.
Checking for dead variables.
Discover errors when you write the code, not when you run it.
And so much more...
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.
Set it up first.
Then write some code and bundle with packer::bundle()
,
which produces the JavaScript code.
Scaffolds are central to packer. They create the necessary structure to use webpack and npm with R.
e.g.:
packer::scaffold_golem()
The term "scaffold" was blatantly stolen from htmlwidgets::scaffoldWidget
.
Source code is written in ./srcjs
and is bundled to ./inst
packer::bundle()
{ggplot2}
{ggplot2}
by JS driven visualization. We call:
packer::scaffold_golem(framework7 = TRUE)
Sets a Framework7 compatible structure for {golem}
:
./srcjs
.This is a very basic app but it works well.
packer::bundle()pkgload::load_all()run_app()
Bad news ... it is time for the ...
See you in 5 minutes.
Inside the main ./srcjs/index.js
.
// Import Framework7import Framework7 from 'framework7';// Import Framework7 Stylesimport 'framework7/framework7-bundle.min.css';
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>
The simplest Framework7 layout is composed of:
<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>
Combine HTML and JavaScript code in the same file.
<template> <div class="title" innerHTML=${title}></div></template><script> export default (props) => { const title = 'Hello World'; return $render; }</script>
👎 Difficult to manage.
export default () => { const title = 'Hello World'; return () => ( <div class="title">{title}</div> )}
👍 easy to manage.
$f7
.<Component user="David" id="compo"/>
const Component = (props, context) { const greetings = 'Hello ' + props.user; // render function return () => ( <h1>{greetings}</h1> )}
{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> )}
The remaining of ./srcjs/index.js
:
// ... Other imports ...import App from './components/app.f7.jsx';let app = new Framework7({ el: '#app', theme: 'ios', // specify main app component component: App});
It's time to add an extra component.
selectInput()
.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>
./srcjs/index.js
.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]);
./srcjs/components/widget.f7.jsx
component.data-open-in="sheet"
.// ./srcjs/components/widget.f7.jsxexport 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> )}
widget.f7.jsx
and app.f7.jsx
are in the same folder.___
.// ./srcjs/components/app.f7.jsximport ___ from '___';export default (props, { $f7 }) => { // code omitted ... return () => ( <div id="app"> ... </div> )}
Let's add some <option>
to the <select>
tag...
names
array containing wt
, hp
and qsec
strings.<option>
array.<select>
.Fill in the ___
.
// ./srcjs/components/widget.f7.jsxexport 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> )}
Let's send the selected value to Shiny (1/2).
getSelectValue
: $f7.smartSelect.get()
method.instance.getValue()
.// ./srcjs/components/widget.f7.jsxexport default (props, { $f7 }) => { // ... code from previous step ... // Recover select value const getSelectValue = (id) => { let select = $f7.smartSelect.get('#' + ___); Shiny.___(___, select.getValue()); }; // render function return () => (...)}
Let's send the selected value to Shiny (2/2).
onChange
prop to the <a>
element. onChange
triggered each time a new value is selected.onChange
, call getSelectValue
with relevant ID parameter.Shiny.setInputValue
. // ./srcjs/components/widget.f7.jsxexport 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> )}
./R/app_server.R
.# Inside ./R/app_server.RobserveEvent(input$<ID>, { message(sprintf("Slider value: %s", input$<ID>))})
packer::bundle()pkgload::load_all()run_app()
packer::bundle()pkgload::load_all()run_app()
🏆 Congrats! You designed your first component. Let's improve it by adding some R logic.
See you in 5 minutes.
mtcars
dataset and {ggplot2}
.lm(mpg ~ input$var, mtcars)
../R/app_server.R
.___
and ...
.output$plot <- renderPlot({ ggplot( data = ___, mapping = aes(x = mpg, y = .data[[___]]) ) + geom_...() + geom_smooth()})
That's all folks!
output$plot
works by pair with renderPlot("plot")
...app_ui.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
.
{ggplot2}
chart by JS code.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./srcjs/components/widget.f7.jsx
.packer::npm_install( c( "echarts", "echarts-gl" ), scope = "prod")
// Import plotting libraryimport * as echarts from 'echarts';import 'echarts-gl';
<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);
./R/process.R
script.process_data()
selects the relevant column based on a given parameter.___
.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", ___)}
./R/app_server.R
, add an observeEvent()
to trigger process_data()
based
on the select input value (JS side).{ggplot2}
code. ./srcjs/components/app.f7.jx
, remove the shiny-plot-output
class from the plot container. observeEvent(___, { ___(___)})
./srcjs/components/widget.f7.jsx
create a new
renderPlot
function, right after the widget component.// ./srcjs/components/widget.f7.jsxexport default (props, { $f7 }) => { // ... renderPlot(); return () => (...)}const renderPlot = () => { // JS logic}
renderPlot
function:shiny:connected
event listener.// ./srcjs/components/widget.f7.jsxlet plot;$(document).on('shiny:connected', () => { // prepare echarts plot plot = echarts.___(document.getElementById(___));});// Resize event$(window).on('resize', function() { plot.resize();});
Shiny.addCustomMessageHandler
to capture the message sent from R.___
.//./srcjs/components/widget.f7.jsxlet plotOptions, p, data;Shiny.addCustomMessageHandler(___, (___) => { p = message.var; data = message.___;});
___
.// ./srcjs/components/widget.f7.jsxplotOptions = { title: { text: 'Plot' }, tooltip: {}, legend: { data:['mpg', p] }, xAxis: { data: message.data[___] }, yAxis: { type: 'value' }, series: [ { name: p, type: 'scatter', data: message.data[___] } ]}___.setOption(___);
David
Data Scientist at Novartis
@divadnojnarg
John
Software Engineer at Opifex
@jdatap
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 |
o | Tile View: Overview of Slides |
Alt + f | Fit Slides to Screen |
Esc | Back to slideshow |
David
Data Scientist at Novartis
@divadnojnarg
John
Software Engineer at Opifex
@jdatap
We're in for 2 hours of fun!
Clone this repository with the RStudio IDE or via the command line.
git clone https://github.com/RinteRface/unleash-shiny-2021.gitcd 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:
{shinyMobile}
development >=
2 years ...✅ R code written in 4.0.0
will (likely) run on 3.0.0
+
💀 JavaScript not so much.
Code size matters in JavaScript: the smaller the file the faster it loads.
Can be written and read by a human.
function addOne(xyz){ return xyz + 1;}addOne(2);
Loads faster but can't be written or read by a human.
function addOne(n){return n+1}addOne(2);
We need to pre-process the code to:
It does not end here.
Pre-processing enables even more.
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.
Checking for dead variables.
Discover errors when you write the code, not when you run it.
And so much more...
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.
Set it up first.
Then write some code and bundle with packer::bundle()
,
which produces the JavaScript code.
Scaffolds are central to packer. They create the necessary structure to use webpack and npm with R.
e.g.:
packer::scaffold_golem()
The term "scaffold" was blatantly stolen from htmlwidgets::scaffoldWidget
.
Source code is written in ./srcjs
and is bundled to ./inst
packer::bundle()
{ggplot2}
{ggplot2}
by JS driven visualization. We call:
packer::scaffold_golem(framework7 = TRUE)
Sets a Framework7 compatible structure for {golem}
:
./srcjs
.This is a very basic app but it works well.
packer::bundle()pkgload::load_all()run_app()
Bad news ... it is time for the ...
See you in 5 minutes.
Inside the main ./srcjs/index.js
.
// Import Framework7import Framework7 from 'framework7';// Import Framework7 Stylesimport 'framework7/framework7-bundle.min.css';
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>
The simplest Framework7 layout is composed of:
<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>
Combine HTML and JavaScript code in the same file.
<template> <div class="title" innerHTML=${title}></div></template><script> export default (props) => { const title = 'Hello World'; return $render; }</script>
👎 Difficult to manage.
export default () => { const title = 'Hello World'; return () => ( <div class="title">{title}</div> )}
👍 easy to manage.
$f7
.<Component user="David" id="compo"/>
const Component = (props, context) { const greetings = 'Hello ' + props.user; // render function return () => ( <h1>{greetings}</h1> )}
{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> )}
The remaining of ./srcjs/index.js
:
// ... Other imports ...import App from './components/app.f7.jsx';let app = new Framework7({ el: '#app', theme: 'ios', // specify main app component component: App});
It's time to add an extra component.
selectInput()
.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>
./srcjs/index.js
.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]);
./srcjs/components/widget.f7.jsx
component.data-open-in="sheet"
.// ./srcjs/components/widget.f7.jsxexport 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> )}
widget.f7.jsx
and app.f7.jsx
are in the same folder.___
.// ./srcjs/components/app.f7.jsximport ___ from '___';export default (props, { $f7 }) => { // code omitted ... return () => ( <div id="app"> ... </div> )}
Let's add some <option>
to the <select>
tag...
names
array containing wt
, hp
and qsec
strings.<option>
array.<select>
.Fill in the ___
.
// ./srcjs/components/widget.f7.jsxexport 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> )}
Let's send the selected value to Shiny (1/2).
getSelectValue
: $f7.smartSelect.get()
method.instance.getValue()
.// ./srcjs/components/widget.f7.jsxexport default (props, { $f7 }) => { // ... code from previous step ... // Recover select value const getSelectValue = (id) => { let select = $f7.smartSelect.get('#' + ___); Shiny.___(___, select.getValue()); }; // render function return () => (...)}
Let's send the selected value to Shiny (2/2).
onChange
prop to the <a>
element. onChange
triggered each time a new value is selected.onChange
, call getSelectValue
with relevant ID parameter.Shiny.setInputValue
. // ./srcjs/components/widget.f7.jsxexport 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> )}
./R/app_server.R
.# Inside ./R/app_server.RobserveEvent(input$<ID>, { message(sprintf("Slider value: %s", input$<ID>))})
packer::bundle()pkgload::load_all()run_app()
packer::bundle()pkgload::load_all()run_app()
🏆 Congrats! You designed your first component. Let's improve it by adding some R logic.
See you in 5 minutes.
mtcars
dataset and {ggplot2}
.lm(mpg ~ input$var, mtcars)
../R/app_server.R
.___
and ...
.output$plot <- renderPlot({ ggplot( data = ___, mapping = aes(x = mpg, y = .data[[___]]) ) + geom_...() + geom_smooth()})
That's all folks!
output$plot
works by pair with renderPlot("plot")
...app_ui.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
.
{ggplot2}
chart by JS code.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./srcjs/components/widget.f7.jsx
.packer::npm_install( c( "echarts", "echarts-gl" ), scope = "prod")
// Import plotting libraryimport * as echarts from 'echarts';import 'echarts-gl';
<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);
./R/process.R
script.process_data()
selects the relevant column based on a given parameter.___
.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", ___)}
./R/app_server.R
, add an observeEvent()
to trigger process_data()
based
on the select input value (JS side).{ggplot2}
code. ./srcjs/components/app.f7.jx
, remove the shiny-plot-output
class from the plot container. observeEvent(___, { ___(___)})
./srcjs/components/widget.f7.jsx
create a new
renderPlot
function, right after the widget component.// ./srcjs/components/widget.f7.jsxexport default (props, { $f7 }) => { // ... renderPlot(); return () => (...)}const renderPlot = () => { // JS logic}
renderPlot
function:shiny:connected
event listener.// ./srcjs/components/widget.f7.jsxlet plot;$(document).on('shiny:connected', () => { // prepare echarts plot plot = echarts.___(document.getElementById(___));});// Resize event$(window).on('resize', function() { plot.resize();});
Shiny.addCustomMessageHandler
to capture the message sent from R.___
.//./srcjs/components/widget.f7.jsxlet plotOptions, p, data;Shiny.addCustomMessageHandler(___, (___) => { p = message.var; data = message.___;});
___
.// ./srcjs/components/widget.f7.jsxplotOptions = { title: { text: 'Plot' }, tooltip: {}, legend: { data:['mpg', p] }, xAxis: { data: message.data[___] }, yAxis: { type: 'value' }, series: [ { name: p, type: 'scatter', data: message.data[___] } ]}___.setOption(___);