Tutorial: Create your first Rhino app
Source:vignettes/tutorial/create-your-first-rhino-app.Rmd
create-your-first-rhino-app.Rmd
Setup
How to install Rhino?
To get started, the first thing you will need is to install Rhino itself:
install.packages("rhino")
Dependencies
This tutorial uses the native pipe operator (|>
)
introduced in R 4.1 release. If you use an earlier R version, then you
can use the %>%
pipe operator found in
magrittr and dplyr packages instead.
To use the state of the art JavaScript and Sass development tools provided by Rhino, you’ll need to install Node.js (v16 or later) on your system.
Rhino will still work without Node.js but with some limitations (described in JavaScript and Sass sections).
Create an initial application
Creating a new Rhino application can be done in two ways - by running
rhino::init()
function or by using the RStudio Create
Project functionality.
Create an application using the RStudio wizard
If you use RStudio, probably the easiest way to create a new Rhino application is to simply use Create New Project feature. Once Rhino is installed, it will be automatically added as one of the options in RStudio:
Choose it, input the new project name and you are ready to go.
Create an application using rhino::init()
Creating a Rhino application is possible in the R console by running
the init
function:
rhino::init("RhinoApplication")
There are two things you need to know when choosing this way of initializing your application:
- Rhino will not change your working directory. You need to either open a new R session in your new application directory or manually change the working directory.
setwd("./RhinoApplication")
- Rhino relies on options added to the projects
.Rprofile
file. The most robust way to make sure it was correctly sourced is to simply restart the R session.
A result of both paths will be an initial Rhino application with the following structure:
.
├── app
│ ├── js
│ │ └── index.js
│ ├── logic
│ │ └── __init__.R
│ ├── static
│ │ └── favicon.ico
│ ├── styles
│ │ └── main.scss
│ ├── view
│ │ └── __init__.R
│ └── main.R
├── tests
│ ├── cypress
│ │ └── e2e
│ │ └── app.cy.js
│ ├── testthat
│ │ └── test-main.R
│ └── cypress.json
├── app.R
├── RhinoApplication.Rproj
├── dependencies.R
├── renv.lock
└── rhino.yml
If you want to know more about it, check this document.
Running the application
Now, once you are all set up, let’s run it:
shiny::runApp()
And here is what you should be seeing right now:
Add your first module
Your application runs, but it doesn’t have any meaningful functionality. Let’s add something there!
Module structure
In Rhino, each application view is intended to live as a Shiny module
and use encapsulation provided by box
package.
Rhino already created a good place for new modules, the
app/view
directory. Create a file there, named
chart.R
:
Calling a module
The next step is to call your new module in your application. First,
you need to import it into your main application file. To do that, add
another box::use
section in your app/main.R
file:
# app/main.R
box::use(
app/view/chart,
)
...
Now, the main module will be able to use exported functions from
chart.R
. Let’s try it! Modify your app/main.R
file to look like that:
# app/main.R
box::use(
shiny[bootstrapPage, moduleServer, NS],
)
box::use(
app/view/chart,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
chart$ui(ns("chart"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
chart$server("chart")
})
}
Now, when you run your application, you should see the message from the newly created module:
Adding components to a module
Now is the time to start adding something to your new module. What can we add to a “chart” module? You’re right, a chart. Let’s add a chart with a rhinoceros dataset available in Rhino.
Adding R packages
First, we need to install a library for visualizations - for that, we
will go with echarts4r
.
We will be using a total of 5 packages for this application. To save us time in the tutorial we will install them all here.
# In R console
rhino::pkg_install(c("dplyr", "echarts4r", "htmlwidgets", "reactable", "tidyr"))
This function will install the packages, and update
dependencies.R
and renv.lock
files
accordingly.
Note: Package htmlwidgets
is
already installed since it is a dependency for shiny
, but
we still should add it to the dependencies.R
file.
Add dependencies to the module
Now, once you have both packages available in your project
environment, it’s time to use them. First, you need to import them into
your module. Extend box::use
call in your
app/view/chart.R
file:
# app/view/chart.R
box::use(
echarts4r,
shiny[h3, moduleServer, NS, tagList],
)
...
You can use those packages in your module by calling
{package}${function}
. For more options of importing in
box
check this link.
Add echarts4r
render to the server part of the module
and output part to its UI:
# app/view/chart.R
box::use(
echarts4r,
shiny[h3, moduleServer, NS, tagList],
rhino[rhinos],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Chart"),
echarts4r$echarts4rOutput(ns("chart"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
output$chart <- echarts4r$renderEcharts4r(
rhinos |>
echarts4r$group_by(Species) |>
echarts4r$e_chart(x = Year) |>
echarts4r$e_line(Population) |>
echarts4r$e_x_axis(Year) |>
echarts4r$e_tooltip()
)
})
}
One thing worth noting here is that in the UI part we had to use
another function from Shiny - tagList
. To be able to do
that, you have to adjust your import in box::use
- simply
add tagList
to the list of imported functions.
Finally, when you run your application, you should see something similar to this:
Add a second module
Once you have some content presented in the application, it would be great to add a table to show the dataset.
For that, let’s create another module -
app/view/table.R
:
# app/view/table.R
box::use(
shiny[h3, moduleServer, NS, tagList],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Table")
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
})
}
Calling the second module
As we did before, we need to call the new module in the
main.R
file:
# app/main.R
box::use(
shiny[bootstrapPage, moduleServer, NS],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
table$ui(ns("table")),
chart$ui(ns("chart"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
table$server("table")
chart$server("chart")
})
}
Use the same dataset for both modules
We want to use the same dataset in both modules, so instead of calling it twice, let’s pass data as an argument:
# app/main.R
box::use(
shiny[bootstrapPage, moduleServer, NS],
rhino[rhinos],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
table$ui(ns("table")),
chart$ui(ns("chart"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
data <- rhinos
table$server("table", data = data)
chart$server("chart", data = data)
})
}
# app/view/table.R
box::use(
shiny[h3, moduleServer, NS, tagList],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Table")
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
})
}
# app/view/chart.R
box::use(
echarts4r,
shiny[h3, moduleServer, NS, tagList],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Chart"),
echarts4r$echarts4rOutput(ns("chart"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$chart <- echarts4r$renderEcharts4r(
data |>
echarts4r$group_by(Species) |>
echarts4r$e_chart(x = Year) |>
echarts4r$e_line(Population) |>
echarts4r$e_x_axis(Year) |>
echarts4r$e_tooltip()
)
})
}
Create a table
For the table, we will go with the reactable
package.
Now you can add a table to the application. Let’s check the raw data for Rhinos:
# app/view/table.R
box::use(
reactable,
shiny[h3, moduleServer, NS, tagList],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Table"),
reactable$reactableOutput(ns("table"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$table <- reactable$renderReactable(
reactable$reactable(data)
)
})
}
The application should look similar to this:
Add logic
It seems that it would be great to slightly adjust the table. Let’s transform the dataset a little bit.
We recommend placing the code which can be expressed without Shiny in
the app/logic
directory.
Let’s create a file there, called
app/logic/data_transformation.R
.
The table would be better if for each Rhino species we would have a
separate column, so it would be easy to compare populations across time.
To do that, we need to transform the dataset using the
pivot_wider
function from the tidyr
package.
Now we are able to access this function in our
data_transformation.R
file using box::use()
.
Let’s also create a function that wraps pivot_wider
and
transforms data. Note that, as always, we need to add @export to be able to access it in the file it
is being sourced.
# app/logic/data_transformation.R
box::use(
tidyr[pivot_wider],
)
#' @export
transform_data <- function(data) {
pivot_wider(
data = data,
names_from = Species,
values_from = Population
)
}
The next step is to call this function in your table module.
Add box import and transform dataset:
# app/view/table.R
box::use(
reactable,
shiny[h3, moduleServer, NS, tagList],
)
box::use(
app/logic/data_transformation[transform_data],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Table"),
reactable$reactableOutput(ns("table"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$table <- reactable$renderReactable(
data |>
transform_data() |>
reactable$reactable()
)
})
}
When you run the application, you should see something similar to this:
You can notice, that table is arranged by the Black Rhino population.
It would make sense to change it to Year using
dplyr::arrange
.
Next, add arrange to transform_data
function:
# app/logic/data_transformation.R
box::use(
dplyr[arrange],
tidyr[pivot_wider],
)
#' @export
transform_data <- function(data) {
pivot_wider(
data = data,
names_from = Species,
values_from = Population
) |>
arrange(Year)
}
The result looks much more understandable:
There is still one element that can be improved. If you check the
X-axis in the chart, values contain a comma. It’s the default behavior
for integers, but this is a year! To fix that, you need to add a custom
formatter. Let’s create another file,
app/logic/chart_utils.R
:
# app/logic/chart_utils.R
box::use(
htmlwidgets[JS],
)
#' @export
label_formatter <- JS("(value, index) => value")
Finally, add the formatter to the chart module:
# app/view/chart.R
box::use(
echarts4r,
shiny[h3, moduleServer, NS, tagList],
)
box::use(
app/logic/chart_utils[label_formatter],
)
#' @export
ui <- function(id) {
ns <- NS(id)
tagList(
h3("Chart"),
echarts4r$echarts4rOutput(ns("chart"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$chart <- echarts4r$renderEcharts4r(
data |>
echarts4r$group_by(Species) |>
echarts4r$e_chart(x = Year) |>
echarts4r$e_line(Population) |>
echarts4r$e_x_axis(
Year,
axisLabel = list(
formatter = label_formatter
)
) |>
echarts4r$e_tooltip()
)
})
}
It should now look better:
Add custom styles
Note: Sass builder uses Node.js. If you are not
able to install Node in your environment, you can change the
sass
entry in the rhino.yml
file to
r
. It will now use the R package for Sass bundling. Under
the hood, it uses a deprecated C++ library, so the Node solution is
strongly recommended here.
In this stage, the application has working components, but it doesn’t have a clean and organized look to it. For this we will need a little CSS styling.
Adjusting application style can be done by providing custom styles in
the app/styles
directory, But first, you need to adjust the
application a little bit by adding HTML tags and CSS classes:
# app/main.R
box::use(
shiny[bootstrapPage, div, moduleServer, NS],
rhino[rhinos],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
div(
class = "components-container",
table$ui(ns("table")),
chart$ui(ns("chart"))
)
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
data <- rhinos
table$server("table", data = data)
chart$server("chart", data = data)
})
}
# app/view/chart.R
box::use(
echarts4r,
shiny[div, moduleServer, NS],
)
box::use(
app/logic/chart_utils[label_formatter],
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
class = "component-box",
echarts4r$echarts4rOutput(ns("chart"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$chart <- echarts4r$renderEcharts4r(
data |>
echarts4r$group_by(Species) |>
echarts4r$e_chart(x = Year) |>
echarts4r$e_line(Population) |>
echarts4r$e_x_axis(
Year,
axisLabel = list(
formatter = label_formatter
)
) |>
echarts4r$e_tooltip()
)
})
}
# app/view/table.R
box::use(
reactable,
shiny[div, moduleServer, NS],
)
box::use(
app/logic/data_transformation[transform_data],
)
#' @export
ui <- function(id) {
ns <- NS(id)
div(
class = "component-box",
reactable$reactableOutput(ns("table"))
)
}
#' @export
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$table <- reactable$renderReactable(
data |>
transform_data() |>
reactable$reactable()
)
})
}
Now you are ready to modify the styles. Simply add few CSS rules to
app/styles/mains.scss
file:
// app/styles/main.scss
.components-container {
display: inline-grid;
grid-template-columns: 1fr 1fr;
width: 100%;
.component-box {
padding: 10px;
margin: 10px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
If you try running the application right now, you will not see any
changes. That is because Rhino uses minified
app/static/app.min.css
for styling. To use it, you will
need to build it using the Rhino function:
# in R console
rhino::build_sass()
Now, after running the application you should see something similar to this:
It is worth noting, that you don’t need to add
app/static/app.min.css
to your application header - Rhino
does that for you.
Let’s adjust the application a little bit more by adding a title:
# app/main.R
box::use(
shiny[bootstrapPage, div, h1, moduleServer, NS],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
h1("RhinoApplication"),
div(
class = "components-container",
table$ui(ns("table")),
chart$ui(ns("chart"))
)
)
}
...
And some styling:
// app/styles/main.scss
.components-container {
display: inline-grid;
grid-template-columns: 1fr 1fr;
width: 100%;
.component-box {
padding: 10px;
margin: 10px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
h1 {
text-align: center;
font-weight: 900;
}
Finally, build Sass once again:
# in R console
rhino::build_sass()
The result should look similar to this:
Add JavaScript code
Note: Rhino tools for JS require Node.js. You
can still use JavaScript code like
in a regular Shiny application, but instead of using
www
directory, you should add your files to
static/js
and call them using full path,
e.g. tags$script(src = "static/js/app.min.js")
.
As the last element, let’s add a button that will trigger a JavaScript popup.
First, we need to create a simple button and style it:
# app/main.R
box::use(
shiny[bootstrapPage, div, h1, icon, moduleServer, NS, tags],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
h1("RhinoApplication"),
div(
class = "components-container",
table$ui(ns("table")),
chart$ui(ns("chart"))
),
tags$button(
id = "help-button",
icon("question")
)
)
}
...
// app/styles/main.scss
.components-container {
display: inline-grid;
grid-template-columns: 1fr 1fr;
width: 100%;
.component-box {
padding: 10px;
margin: 10px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
h1 {
text-align: center;
font-weight: 900;
}
#help-button {
position: fixed;
top: 0;
right: 0;
margin: 10px;
}
Remember to rebuild Sass with rhino::build_sass()
!
You should now see a button with a question mark in the top right corner of the application:
Now, it’s time for writing the JavaScript code that should show the
popup with a message. All JS code should be stored in the
app/js
directory. You already have the first (empty) file
there - index.js
. Let’s use it:
// app/js/index.js
export function showHelp() {
alert('Learn more about Rhino: https://appsilon.github.io/rhino/');
}
This function will simply show a browser alert with the message. If
you are familiar with how JavaScript code is used in Shiny applications,
you will notice one difference - keyword export
added
before the function name. In Rhino, only functions marked like that will
be available for Shiny to use.
Same as it was with styles, the Rhino application does not use JS
files directly, but instead utilizes minified version build with
rhino::build_js
function. Try it:
# in R console
rhino::build_js()
Now app/static/js/app.min.js
file has been created and,
same as the minified CSS file, is automatically included in the
application head tag.
The last thing here is to use the showHelp()
function in
the application. To do that, let’s simply add onclick
to
the button:
# app/main.R
box::use(
shiny[bootstrapPage, div, h1, icon, moduleServer, NS, tags],
)
box::use(
app/view/chart,
app/view/table,
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
h1("RhinoApplication"),
div(
class = "components-container",
table$ui(ns("table")),
chart$ui(ns("chart"))
),
tags$button(
id = "help-button",
icon("question"),
onclick = "App.showHelp()"
)
)
}
...
You have probably noticed the second difference between the classic
Shiny approach and the one used in Rhino. All exported JS functions are
now available under App
(same as any other JavaScript
function from a library, e.g. Math.round
).
Now, if you run the application and click the button, you should see something like this:
Congratulations! You now have a fully armed and operational
battle station Rhino application!