Introdução ao Shiny em R
UFPE
Shiny é um framework em linguagem R para a criação de aplicativos web. Por não exigir conhecimento prévio de HTML, CSS e JavaScript, ele democratiza o acesso a essa área de desenvolvimento, permitindo a criação de aplicativos bonitos e complexos a partir de scripts R.
O Shiny fornece uma estrutura para gerarmos código HTML a partir de funções em R. Também possui uma base de JavaScript e CSS para deixar os aplicativos funcionais e com um visual satisfatório. Além disso, podemos utilizar por trás todo o poderio de análise de dados que o R e seus pacotes fornecem. Com esses elementos, já conseguimos construir qualquer tipo de layout e lógica interna.
Contudo, o grande poder do Shiny está em não limitar as possibilidades apenas ao que foi feito pelos desenvolvedores do pacote1. Existem vários outros pacotes criados pela comunidade que trazem mais elementos visuais e funcionais para o Shiny, diminuindo ainda mais a necessidade de conhecermos HTML, CSS e JavaScript. É simples incluir tags HTML, folhas de estilo CSS e suas próprias funções JavaScript em um aplicativo Shiny.
shiny
instalado.Um aplicativo Shiny é um site, uma página na web. Ele terá um endereço (URL) que, quando acessado, exibirá informações em forma de texto e imagens1.
Ser uma página web também significa que ele será constituído de HTML, CSS e JavaScript. Se você não conhece essas linguagens, uma boa maneira de entender o papel de cada uma delas no desenvolvimento de um site é pensar na construção de um prédio.
Podemos pensar o HTML como a estrutura física do prédio: chão, paredes, colunas, teto, encanamento, fiação etc; o CSS é o responsável pela aparência: pintura, pisos, azulejos, decoração em geral; e o JavaScript traz elementos de funcionalidade ao prédio: portas, janelas, interruptores, elevadores etc.
A princípio, você não precisará se preocupar com CSS, pois a aparência padrão do Shiny1 é bem razoável. O mesmo vale para o JavaScript: quando você programa em Shiny, todo o JavaScript necessário para o seu app funcionar corretamente já está pronto e será utilizado automaticamente.
A figura a seguir mostra a UI de um app bem simples, que permite a escolha de duas variáveis e apresenta o gráfico de dispersão delas:
A UI e o servidor são os dois elementos básicos de um aplicativo Shiny. Embora sejam construídos separadamente, um depende do outro e a correta conexão desses componentes é o que gera a interatividade do app. Teremos sempre requisições sendo feitas pela UI, processadas pelo servidor e seus resultados devolvidos à UI. Esse processo está resumido no esquema a seguir:
Esquema UI/servidor de um aplicativo Shiny.
A construção do código HTML que compõe o aplicativo é feito utilizando funções do R que retornam HTML. Esse é um dos papéis do pacote shiny
e um dos desafios da programação em Shiny. Não precisaremos aprender HTML formalmente, mas precisaremos aprender quais funções utilizar para construir uma UI bem estruturada.
Também falamos que o servidor é responsável por receber os códigos que geram as visualizações apresentadas na UI, conectando os inputs e outputs do aplicativo. No código, isso sempre será feito dentro de uma função chamada server
.
Com isso em mente, o código de qualquer aplicativo Shiny terá a estrutura abaixo:
um objeto chamado ui
;
uma função chamada server
;
e uma chamada da função shinyApp()
.
Como exemplo, observe o código a seguir. Ele representa um dos aplicativos mais simples que podemos construir:
Esse código resultará no aplicativo com a seguinte UI:
UI do aplicativo “Olá, mundo!”.
Vejam que a UI se trata apenas de uma página de fundo branco com a frase “Olá, mundo!” escrita no canto superior esquerdo. O app não possui nenhuma interatividade.
Olhando o código anterior, encontramos a frase “Olá, mundo!” na definição do objeto ui
, dentro da função fluidPage()
. Essa função do pacote shiny
é uma entre várias que utilizaremos para retornar código HTML e precisamos utilizá-la para que o HTML do aplicativo funcione corretamente.
O importante por enquanto é sabermos que o objeto ui
é responsável por receber todo o código HTML do nosso aplicativo.
server
sempre receberá os argumentos input
, output
e session
. A partir desses argumentos, montaremos a lógica interna (ou reativa, guarde esse nome) do app. O código que gera as visualizações será escrito dentro dessa função. Como o nosso app Olá, mundo! não possui visualizações ou interatividade, a sua função server
está vazia1.shinyApp(ui, server)
. Essa função vai juntar o objeto ui
e a função server
, construir toda a arquitetura necessária e rodar o aplicativo. Não discutiremos aqui como tudo isso é feito pois envolve tópicos fora do escopo de um material introdutório. Por enquanto, é importante lembrar que essa função sempre deve ser chamada ao fim do código de um Shiny app.app.R
. Recomendamos fortemente que vocês utilizem projetos e que, neste primeiro momento, sempre salvem o script dos aplicativos na raiz do projeto.Como discutimos anteriormente, um aplicativo Shiny em funcionamento sempre terá um computador rodando uma sessão de R por trás. Esse computador, chamado genericamente de servidor, pode ser uma máquina virtual em um serviço de nuvem, uma máquina virtual em um serviço de hospedagem, um servidor dentro da sua empresa, um servidor na sua casa ou mesmo o seu próprio computador pessoal.
Normalmente, enquanto estamos desenvolvendo um aplicativo Shiny, queremos testá-lo localmente para verificar se tudo funciona corretamente, se está ficando bonito ou simplesmente para gastar alguns minutos apreciando a nossa obra de arte. Testar localmente significa que o seu próprio computador fará as vezes de servidor, embora isso não signifique que seu app ficará disponível na internet.
Quando servimos um app localmente, isto é, quando rodamos um app, ganhamos um endereço que será acessível apenas do nosso computador. A partir desse endereço, podemos testar nosso app no navegador, como se ele já estivesse em produção. No RStudio, para rodar nossos apps, utilizamos justamente o botão Run App.
Se você voltar ao RStudio, eventualmente vai notar algo muito importante: a sua sessão de R estará ocupada! Mas isso não deveria ser uma surpresa, pois já discutimos que todo Shiny app tem uma sessão de R rodando por trás.
Essa sessão fornece a comunicação da UI (ou do nosso navegador) com o servidor e é responsável por atualizar as visualizações apresentadas na UI, sempre que alguém interagir com o app. Embora o nosso app Olá, mundo não possuir interatividade, a estrutura necessária para que a interatividade aconteça ainda assim é criada pelo Shiny.
Para liberar a sessão, basta clicar no botão “stop”, na parte de cima do Console, ou pressionar a tecla Esc
. Veja que, ao fazer isso, a tela do app ficará cinza, indicando que ele foi desconectado do servidor e não funcionará mais corretamente.
http://127.0.0.1
)1 com alguma porta que esteja disponível escolhida aleatoriamente (:4028
). Esse endereço aparecerá no nosso navegador e poderemos copiá-lo e colá-lo em qualquer outra aba ou navegador que quisermos rodar o app.server
.Outputs representam as saídas do nosso aplicativo, isto é, tudo que queremos que nosso código R retorne para a UI. Essas saídas podem ser tabelas, gráficos, mapas, texto, imagens ou qualquer outro elemento HTML.
Os outputs são definidos na UI e criados no server. Cada tipo de output é definido por uma função do tipo *Output()
. Veja as principais funções dessa família:
Essas funções especificam onde os outputs serão colocados dentro da UI. Elas não especificam como eles serão criados. Para isso, utilizamos as funções do tipo render*()
, responsáveis por definir o código R que gera cada output. Além disso, elas renderizam os resultados para HTML, possibilitando que essas visualizações sejam inseridas no código HTML que gera a UI. Na grande maioria dos casos, teremos o par visualizacaoOutput()
e renderVisualizacao()
.
Veja a seguir as principais funções render*()
e como elas se comunicam com as funções *Output()
.
O argumento outputId
das funções *Output()
é utilizado para identificarmos cada output dentro da função server
. Todos os outputs criados ficarão dentro da lista output
. Veja a seguir, o exemplo de um aplicativo que contém um histograma da variável mpg
da base mtcars
.
No código anterior:
a função plotOutput()
especifica o lugar na UI onde será colocado o histograma (no caso, logo abaixo do texto "Histograma da variável mpg"
);
para criar o histograma, atribuímos o resultado da função renderPlot()
ao valor histograma
da lista output
, mesmo nome dado ao argumento outputId
na função plotOutput()
;
a função renderPlot()
, assim com qualquer outra função da família render*()
, recebe como primeiro argumento o código para gerar o output;
o histograma é gerado com o código hist(mtcars$mpg)
.
shiny
possibilita diversas opções de widgets2, a depender do tipo de valor a ser passado.Current Value(s)
é mostrado qual valor será levado para dentro da função server
em cada caso.*Input()
ou *Button
.outputId
das funções *Output()
, todas essas funções possuem inputId
como primeiro argumento, que recebe uma string e será utilizado para acessar dentro da função server
cada um dos inputs criados. Isso implica que dois inputs não podem ter o mesmo inputId
.library(shiny)
ui <- fluidPage(
"Histograma da variável mpg",
selectInput(
inputId = "variavel",
label = "Selecione uma variável",
choices = names(mtcars)
),
plotOutput(outputId = "histograma")
)
server <- function(input, output, session) {
output$histograma <- renderPlot({
hist(mtcars[[input$variavel]])
})
}
shinyApp(ui, server)
A caixa de seleção foi criada pela função selectInput()
. Essa função requer 3 argumentos:
o inputId
, como discutido anteriormente;
o label
, que será mostrado na tela e indica a quem estiver usando o app o que está sendo escolhido nesse input1;
e o choices
, um vetor com as possíveis escolhas da caixa de seleção.
Para acessar o valor do input na função server
, utilizamos a lista input
e o nome dado no argumento inputId
da função selectInput()
(no caso, "variavel"
). A lista input
guarda todos os inputs criados na UI.
selectInput()
envia ao servidor uma string com o valor escolhido na caixa de seleção. Por isso utilizamos o operador [[
para fazer a seleção da variávelinput$variavel
recebe o valor "mpg"
e, por consequência, mtcars[[input$variavel]]
será igual a mtcars[["mpg"]]
, que retorna um vetor com os valores da coluna mpg
e será utilizado pela função hist()
para gerar o gráfico. [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2 10.4
[16] 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4 15.8 19.7
[31] 15.0 21.4
library(shiny)
variaveis <- names(mtcars)
ui <- fluidPage(
selectInput(
inputId = "variavel_A",
label = "Variável A",
choices = variaveis
),
plotOutput(outputId = "histograma_A"),
selectInput(
inputId = "variavel_B",
label = "Variável B",
choices = variaveis,
selected = variaveis[2],
),
plotOutput(outputId = "histograma_B")
)
server <- function(input, output, session) {
output$histograma_A <- renderPlot({
print("Gerando histograma A...")
hist(mtcars[[input$variavel_A]], main = "Histograma A")
})
output$histograma_B <- renderPlot({
print("Gerando histograma B...")
hist(mtcars[[input$variavel_B]], main = "Histograma B")
})
}
shinyApp(ui, server)
O destino final de aplicativos Shiny costuma ser um ambiente de produção diferente do ambiente de desenvolvimento. Seja um servidor próprio, uma máquina na nuvem ou o shinyapps.io, o nosso app precisa funcionar nesses ambientes, não apenas na nossa máquina.
Uma vez no ambiente de produção, aplicativos Shiny costumam ficar lá por um bom tempo, gerando a necessidade de manutenção períodica e/ou atualizações. A depender de como o app foi desenvolvido, essas tarefas podem ficar muito mais trabalhosas. Seria interessante, nesse sentido, ter um framework de desenvolvimento que facilitasse a organização e documentação do código e o controle das dependências. É para isso que o Golem foi criado.
O Golem é um framework para desenvolvimento de aplicativos Shiny prontos para serem colocados em produção. As vantagens são:
padroniza a organização dos scripts e demais arquivos do seu app;
integra com pacotes que aceleram o desenvolvimento do código;
motiva a documentação do código;
e facilita o compartilhamento e a reutilização de códigos em outros projetos e com outras pessoas.
Vamos ver como podemos utilizar o pacote golem
para obter essas vantagens.
Antes de mais nada, precisamos instalar o pacote.
Para criar um app dentro do framework Golem, basta rodar o seguinte código:
Esse código vai criar uma pasta chamada meuapp/
dentro de ~/Documents/
(você pode especificar qualquer outra pasta no seu computador). Essa pasta vai conter diversos arquivos que lhe permitirão iniciar o desenvolvimento do seu app dentro do Golem.
golem
diz muito mais respeito a seguir uma filosofia do que a aprender uma ferramenta. Como os próprios autores descrevemGolem is an opinionated framework for building production-grade shiny applications.
golem::create_golem()
, você poderia continuar o desenvolvimento do app dentro desse framework sem utilizar nenhuma outra função do golem
1.O que realmente importa é seguir as seguintes premissas:
um aplicativo Golem é construído como um pacote R;
sempre que conveniente, devemos dividir o nosso app em módulos;
e devemos documentar as funções que compõem o aplicativo.
Para entender melhor o Golem, precisamos falar um pouco de pacotes.
Um pacote de R é uma forma específica de organizar código, seguindo o protocolo descrito pela R Foundation1.
Pacotes são unidades fundamentais de código R reprodutíveis.
— Wickham & Bryan
Um pacote inclui funções em R, documentação sobre como usá-las, testes e dados de exemplo.
De maneira geral, as funções de um pacote tentam resolver bem um problema em específico. O pacote dplyr
, por exemplo, possui funções especializadas em manipular bases de dados, já o pacote ggplot2
possui funções para a construção de gráficos.
A seguir, apresentaremos a estrutura básica (arquivos e pastas) de qualquer pacote R.
DESCRIPTION
: define o nome, descrição, versão, licença, dependências e outras caracaterísticas do pacote. É um arquivo de metadados.
LICENSE
: especifica os termos de uso e distribuição do seu pacote.
.Rbuildignore
: lista arquivos que não devem ser incluídos ao compilar o pacote R a partir do código-fonte, isto é, arquivos que são úteis apenas no desenvolvimento e não serão enviados para quem instalar o pacote.
NAMESPACE
: este arquivo declara as funções que o pacote exporta (que ficam disponível quando alguém usa library()
) e as funções que seu pacote importa de outros pacotes. Ele é criado automaticamente a partir da documentação das funções do pacote. Não devemos editar este arquivo manualmente.
R/
: pasta onde fica o código R das funções do pacote. Essa pasta não deve conter subdiretórios.
Uma forma de criarmos a estrutura básica de um pacote é usamos a função usethis::create_package()
.
É preciso informar um caminho como ~/Documents/meupacote
e uma nova pasta chamada meupacote
será criada dentro da pasta Documents
. Essa pasta será tanto um projeto do RStudio quanto um pacote, ambos chamados meupacote
.
Evite sempre adicionar acentos, caracteres especiais e espaços no nome do pacote, assim como nos arquivos que criar dentro dele.
R/
Dentro de um pacote, a pasta R/
só pode ter scripts R com funções.
Todas as funções que farão parte do pacote devem estar nessa pasta, mesmo que elas sejam apenas funções usadas internamente.
As funções colocadas dentro dessa pasta nunca devem ser rodadas diretamente.
Se você quiser testá-las, deve fazer isso “carregando as funções”, isto é, usando a função devtools::load_all()
.
Isso fará com que todas as funções dentro da pasta R/
fiquem disponíveis na sua sessão, algo equivalente a fazer library(meupacote)
, mas com a diferença de também carregar as funções não exportadas.
Para criar um arquivo script R dentro da pasta R/
, deve usar usethis::use_r("nome-do-arquivo")
.
Quando estamos construindo um script R, é comum querermos utilizar dentro dele outros pacotes que não apenas o R base.
Em geral utilizamos library(pacote)
para carregar esses pacotes.
Quando estamos construindo um pacote, bibliotecas externas são chamadas de dependências.
Ao desenvolver um pacote, a função library()
nunca deve ser utilizada1;
Todas as funções externas devem ter seus pacotes de origem explicitamente referenciados pelo operador ::
.
Sempre que você utilizar um pacote dentro do pacote que está desenvolvendo, você deve especificá-lo como dependência no arquivo DESCRIPTION
.
Isso informará ao R que, ao instalar o seu pacote, ele também precisa instalar todos os pacotes listados como dependência nesse arquivo.
Você pode fazer isso facilmente utilizando usethis::use_package()
. O código abaixo registra o pacote dplyr
como dependência de um pacote sendo construído.
usethis::use_dev_package()
para adicioná-lo como dependência.Se o seu pacote possuir bases de dados, como a dplyr::starwars
, ou qualquer outro tipo de objeto do R, como pi
ou letters
, você deve colocá-los dentro de uma pasta chamada data/
, na raiz do projeto, com a extensão .rda
1.
Isso pode ser feito a partir da função usethis::use_data()
.
Ao rodar o código abaixo, por exemplo, vamos criar uma pasta data/
na raiz do pacote, caso ela não exista ainda, e salvar nela o vetor base nomes
no arquivo nomes.rda
.
nomes
ficará disponível para ser utilizado (igual a base starwars
fica disponível quando carregamos o dplyr
).Para documentar as funções do seu pacote (i.e., gerar aquele documento mostrado quando rodamos ?mean
, por exemplo), escrevemos comentários antes da definição da função nos scripts da pasta R/
. Fazemos isso usando um tipo de comentários especial, o #'
, e marcadores que indicam qual parte da documentação estamos escrevendo. A estrutura dos comentários deve ser a seguinte:
O marcador @export
indica que a função ficará disponível quando rodarmos library(meupacote)
. Se você não quer que a função fique disponível, basta não colocar esse marcador1.
Após escrever a documentação das suas funções dessa maneira, você deve rodar devtools::document()
para que ela seja compilada e fique disponível no seu pacote (acessível pelo Help do R). Isso é feito pelo pacote roxygen2
.
Dica: o RStudio disponibiliza um atalho para criar a estrutura da documentação de uma função. No menu superior, clique em
Code
->Insert Roxygen Skeleton
.
Para verificar se desenvolvimento do pacote está de acordo com o exigido, é possivel utilizar a função devtools::check()
.
Essa função devolverá um relatório com possíveis problemas que o seu pacote pode ter, como erros de sintaxe, arquivos com extensões não permitidos, dependências não declaradas ou erros de documentação.
Para instalar o seu pacote localmente durante o desenvolvimento, rode a função devtools::install()
. Isso é equivalente a ter o pacote instalado via install.packages()
.
Uma das formas de disponibilizar o seu pacote na internet é subi-lo para um repositório público no Github. Dessa maneira, qualquer pessoa pode instalá-lo com a função
remotes::install_github()
. Para subir um pacote para o CRAN, o processo é mais burocrático. Mas interessante se deseja atingir toda a comunidade científica que utiliza o R e instala os pacotes peloinstall.packages()
.
Agora que já sabemos o básico sobre pacotes R, podemos voltar a falar do Golem.
Uma pasta criada pela função golem::create_golem()
terá a seguinte estrutura:
#> ├── 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
Vejamos cada arquivo mais detalhadamente e discutir a importância dele no contexto do desenvolvimento de um Shiny app:
O arquivo DESCRIPTION
: guarda os metadados do pacote. No desenvolvimento de um aplicativo Shiny, ele vai guardar o nome do aplicativo, o que ele faz, as dependências dele, a versão (importante em projetos em produção que recebem atualizações periódicas) e quem contatar quando alguma coisa der errada. Com relação às dependências, isso quer dizer que, para rodar o seu app, o R precisará instalar todos os pacotes listados nesse arquivo.
O arquivo NAMESPACE
: guarda metadados do pacote. Com esse arquivo, podemos carregar apenas funções específicas de um pacote dentro do nosso app1. O Golem faz isso com o pacote shiny
nas funções app_ui()
e app_server()
para não precisarmos colocar shiny::
no início de cada função.
A pasta R/
: guarda as funções do pacote. Como o app será feito dentro de um pacote R, todo o seu código será escrito em funções nessa pasta. O Golem já cria os arquivos para construirmos a UI e o servidor. Os scripts contendo os módulos do aplicativo também devem ser colocados nessa pasta, assim como scripts com funções úteis utilizadas em vários lugares do app.
O arquivo R/app_config.R
: usado para especificar alguns mecanismos do Golem, como ler o arquivo de configuração localizado em inst/golem-config.yml
.
O arquivo R/app_server.R
: script com a função app_server()
, onde será desenvolvido o servidor do seu aplicativo.
O arquivo R/app_ui.R
: script com a função app_ui()
, onde será desenvolvido a UI do seu aplicativo, e a função golem_add_external_resources()
, utilizada para dizer ao Shiny que a pasta inst/app/www
será utilizada como uma fonte de recursos externos, acessada pelo caminho www/nome_do_arquivo
1. Além disso, o Golem inclui no HTML do seu app a conexão com todo arquivo CSS e JS que você coloca nessa pasta, então não precisamos fazer isso manualmente.
O arquivo R/run_app.R
: script que contém a função run_app()
, utilizada para rodar o app. Ela chama a função shiny::shinyApp()
, que inicia o app localmente. A funcão shiny::shinyApp()
está dentro da função golem::with_golem_options()
, que recebe parâmetros passados para a run_app()
. Esses parâmetros podem ser recuperados dentro do app com a função golem::get_golem_options()
, deixando a parametrização de um aplicativo Shiny muito mais simples1.
dev/
: pasta com scripts do golem
que podem ser utilizados ao longo do desenvolvimento do app. Eles contêm uma lista de funções úteis que ajudam a configurar diversos aspectos do aplicativo. O uso desses scripts é opcional.
A pasta inst/app/www
: local onde adicionaremos os recursos externos do aplicativo (imagens, arquivos CSS, fontes etc) que serão compartilhados com o navegador de quem estiver usando o app. A pasta inst
é uma pasta especial no desenvolvimento de pacotes. Ela serve para adicionarmos arquivos que gostaríamos que fossem instalados com o pacote, como arquivos de teste, imagens etc. No contexto do Shiny, ela será utilizada para guardarmos arquivos auxiliares, como a própria pasta app/www
, templates .Rmd
de relatórios que o app gera, arquivos .md
com textos que serão colocados no app, entre outros.
A pasta man/
: contém a documentação do pacote, a ser gerada pelo roxygen2
. É muito importante documentarmos todas as funções do nosso app, pois é muito comum que o código precise de ajustes ou atualizações no futuro. Uma breve descrição do que a função espera e o que ela devolve pode ser suficiente para ajudar a pessoa que for mexer no app no futuro (que pode ser você mesma) a economizar horas de debug.
OBRIGADO!
Slide produzido com quarto
Baseado noLivro: https://programando-em-shiny.curso-r.com/
Livro: R Packages
Tópicos Especiais em Estatística Computacional - Prof. Jodavid Ferreira