-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathshiny-na-pratica-1.qmd
More file actions
446 lines (328 loc) · 16 KB
/
Copy pathshiny-na-pratica-1.qmd
File metadata and controls
446 lines (328 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# Shiny na prática I {#sec-shiny-pratica-1}
Neste capítulo, vamos fazer uma pequena pausa na apresentação de conceitos e abordar alguns temas práticos muito úteis no desenvolvimento de aplicativos Shiny.
## Importando bases de dados
A tarefa de importação de dados dentro do Shiny não possui nenhum desafio particular. Podemos seguir lendo nossos dados, seja de arquivos locais, da internet ou de banco de dados, da mesma maneira que fazemos em scripts R habituais.
A única questão importante é onde fazer isso.
### Importar fora da função `server`
Quando importamos os nossos dados fora da função `server`, podemos utilizá-los também na construção da UI. Essa opção é a ideal para quando os seus dados não mudam com o tempo. Veja um exemplo a seguir.
```{r, eval = FALSE}
library(shiny)
link <- "https://raw.githubusercontent.com/williamorim/brasileirao/master/data-raw/csv/matches.csv"
dados <- readr::read_csv(link)
ui <- fluidPage(
titlePanel("Últimos 10 jogos do time"),
sidebarLayout(
sidebarPanel(
selectInput(
"time",
label = "Selecione um time",
choices = unique(dados$home)
)
),
mainPanel(
tableOutput("tabela")
)
)
)
server <- function(input, output, session) {
output$tabela <- renderTable({
dados |>
dplyr::filter(
score != "x",
home == input$time | away == input$time
) |>
dplyr::slice_max(order_by = date, n = 10) |>
dplyr::mutate(
date = format(date, "%d/%m/%Y")
) |>
dplyr::select(date, home, score, away)
})
}
shinyApp(ui, server)
```
### Importando dentro da função `server`
Você pode importar uma base de dados normalmente dentro da função server. Nesse caso, não temos acesso aos dados na construção da UI. Veja no exemplo a seguir que precisamos construir o `selectInput` dentro da UI utilizando a dupla de funções `uiOutput()` e `renderUI()`.
```{r, eval = FALSE}
library(shiny)
ui <- fluidPage(
titlePanel("Últimos 10 jogos do time"),
sidebarLayout(
sidebarPanel(
uiOutput("select_time")
),
mainPanel(
tableOutput("tabela")
)
)
)
server <- function(input, output, session) {
link <- "https://raw.githubusercontent.com/williamorim/brasileirao/master/data-raw/csv/matches.csv"
dados <- readr::read_csv(link)
output$select_time <- renderUI({
selectInput(
"time",
label = "Selecione um time",
choices = unique(dados$home)
)
})
output$tabela <- renderTable({
dados |>
dplyr::filter(
score != "x",
home == input$time | away == input$time
) |>
dplyr::slice_max(order_by = date, n = 10) |>
dplyr::mutate(
date = format(date, "%d/%m/%Y")
) |>
dplyr::select(date, home, score, away)
})
}
shinyApp(ui, server)
```
Quando rodamos esse app, um erro aparece por uma fração de segundos no lugar da tabela. Isso acontece pois, no momento em que o Shiny tenta gerar a tabela pela primeira vez, o `input$time` ainda não existe, ele ainda está sendo criado pelo `renderUI()`. Veremos como lidar com isso na seção de validação do [Capítulo -@sec-reatividade2].
Devemos ler os dados dentro da função `server` quando eles forem atualizados frequentemente, pois dessa maneira eles serão importados no momento em que cada pessoa acessar o app. Importar dentro da função `server` também vai ser necessário quando a leitura dos dados depender de um valor reativo.
No exemplo a seguir, simulamos a conexão com um banco de dados para a leitura das partidas. Também permitimos a escolha da temporada antes da escolha do time. Os dados requisitados ao banco são apenas os da temporada escolhida, diminuindo a quantidade de dados transmitidos. Nesse caso, o objeto `dados` passa a ser uma expressão reativa, pois ele depende do `input$temporada`.
```{r, eval = FALSE}
# Esse exemplo não pode ser rodado,
# pois a função conectar_com_o_banco()
# não existe.
library(shiny)
ui <- fluidPage(
titlePanel("Últimos 10 jogos do time"),
sidebarLayout(
sidebarPanel(
selectInput(
"temporada",
label = "Selecione uma temporada",
choices = 2003:2022
),
uiOutput("select_time")
),
mainPanel(
tableOutput("tabela")
)
)
)
server <- function(input, output, session) {
dados <- reactive({
con <- conectar_com_o_banco()
dplyr::tbl(con, "partidas") |>
dplyr::filter(season == input$temporada) |>
dplyr::collect()
})
output$select_time <- renderUI({
selectInput(
"time",
label = "Selecione um time",
choices = unique(dados()$home)
)
})
output$tabela <- renderTable({
dados() |>
dplyr::filter(
score != "x",
home == input$time | away == input$time
) |>
dplyr::slice_max(order_by = date, n = 10) |>
dplyr::mutate(
date = format(date, "%d/%m/%Y")
) |>
dplyr::select(date, home, score, away)
})
}
shinyApp(ui, server)
```
## Gráficos com `ggplot2` {#sec-ggplot2}
Gráficos são a alegria da festa na hora de comunicarmos os resultados de uma análise de dados. Embora possamos usar tabelas, textos ou caixinhas coloridas com valores para comunicar nossos resultados, são eles que geralmente chamam e prendem a atenção de quem está utilizando o app.
O pacote `ggplot2` é uma ótima ferramenta para produzirmos gráficos no R. Quando entendemos a sua sintaxe, nos tornamos capazes de fazer uma variedade enorme de gráficos e dar a eles a cara que quisermos.
Nada do que o pacote `ggplot2` tem para oferecer se perde quando estamos construindo gráficos dentro de um Shiny app. Pelo contrário, ainda ganhamos um novo recurso!
Para inserir um `ggplot` em um aplicativo Shiny, utilizamos a dupla de funções `plotOutput()`/`renderPlot()`. Essas funções estão preparadas para receber um objeto gerado pelas funções do `ggplot` e renderizá-lo em uma **imagem**, que será inserida no HTML do app por meio da tag `<img>`. Veja um exemplo abaixo.
```{r, eval = FALSE}
library(shiny)
library(ggplot2)
colunas <- names(mtcars)
ui <- fluidPage(
titlePanel("Shiny app com um ggplot"),
sidebarLayout(
sidebarPanel(
selectInput(
"varX",
label = "Variável eixo X",
choices = colunas
),
selectInput(
"varY",
label = "Variável eixo Y",
choices = colunas,
selected = colunas[6]
)
),
mainPanel(
plotOutput("grafico")
)
)
)
server <- function(input, output, session) {
output$grafico <- renderPlot({
ggplot(mtcars, aes(x = .data[[input$varX]], y = .data[[input$varY]])) +
geom_point()
})
}
shinyApp(ui, server)
```
```{r, fig.cap="App com dois inputs especificando as variáveis do eixo x e y de um ggplot .", echo = FALSE}
knitr::include_graphics("img/app_ggplot2.png")
```
O objeto gerado pela função `ggplot()` é uma lista com classe `gg` e `ggplot`, que contém todas as informações necessárias para o R desenhar o gráfico.
```{r}
library(ggplot2)
p <- ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point()
class(p)
names(p)
```
Repare que, se salvássemos o `ggplot` em um arquivo (`.png` por exemplo), poderíamos simplesmente usar a dupla `imageOutput()`/`renderImage()` para inserir um gráfico no nosso app, já que essas funções também criam uma tag `<img>`, mas a partir de um arquivo de imagem local.
Mas não precisar salvar o `ggplot` em um arquivo não é a única vantagem de utilizarmos as funções `plotOutput()`/`renderPlot()`. Essas funções inserem um objeto intermediário no diagrama de reatividade do app: o `plotObj`. Esse objeto é justamente a lista gerada pelas funções que utilizamos na construção do gráfico e que só é recalculado quando um dos inputs existentes no código da função `renderPlot()` muda.
Já o gráfico renderizado depende não apenas desse `plotObj`, mas também do comprimento e altura da janela do navegador de quem estiver utilizando o app. Dessa maneira, o gráfico é renderizado não apenas quando o `plotObj` muda, mas também quando o espaço disponível para a tag `<img>` na tela muda. Nesse segundo caso, o R gera o gráfico novamente, redesenhando seus elementos para a nova proporção de comprimento e altura. E o melhor é que ele faz isso sem precisar rodar o código da função `renderPlot()` novamente, pois tudo o que ele precisa já está salvo no `plotObj`.
```{r, echo = FALSE, fig.cap="Gráfico sendo redimensionado conforme diminuímos o comprimento da tela."}
knitr::include_graphics("img/redimensionando_janela.gif")
```
Sem esse recurso, nossos gráficos seriam apenas imagens sendo esticadas e achatadas, o que provavelmente os deixaria pixelados. Ao contrário do que acontece no R Markdown, em um relatório HTML ou em um flexdashboard por exemplo, no Shiny não precisamos nos preocupar muito com as dimensões de um `ggplot`. Ele será sempre automaticamente otimizado para o espaço disponível na tela.
Mas nem tudo são flores... Por melhor que consigamos mexer no visual do nosso ggplot utilizando a função `theme()`, no fim do dia ele continuará sendo apenas uma imagem no nosso app. Isso significa que não será possível atribuir a ele qualquer comportamento interativo, como *tooltips*, *drildown* ou ações geradas por cliques no gráfico. Para isso, precisaremos utilizar as bibliotecas gráficas próprias para a Web, que utilizam JavaScript para criar esses recursos. Falaremos sobre elas no [Capítulo -@sec-htmlwidgets].
## Animações de carregamento
É muito comum termos visualizações no nosso app que demoram para serem geradas. Quando estamos carregando o app, isso pode fazer com que parte da UI fique em branco, parecendo que a página está quebrada ou fazendo com que alguém passe em branco pelo output que você teve tanto trabalho para fazer. Quando a visualização está sendo recalculada, o padrão do Shiny é deixar a versão anterior acinzentada até que a nova apareça, o que pode gerar estranheza e também passar a ideia de que o aplicativo quebrou.
É uma boa prática sempre avisarmos a quem estiver usando o app que alguma coisa está acontecendo por trás das cortinas. Quando a espera é muito longa, devemos sempre que possível explicar o porquê a pessoa está esperando e dar uma estimativa do tempo. Nesses casos, barras de carregamento são a melhor alternativa. Falaremos delas na próxima seção.
Quando a espera não é tão grande (entre 2 e 10 segundos, por exemplo), animações giratórias ou de looping infinito podem ser utilizadas para indicar que algo vai aparecer ali e reduzir um pouco a percepção do tempo de espera.
Nesta seção, falaremos de dois pacotes que deixam muito simples a tarefa de incluir essas animações em nossos outputs: o `shinycssloaders` e o `shinyWidgets`.
Se você ainda não tem esses pacotes instalados, ambos estão no CRAN:
```{r, eval = FALSE}
install.packages("shinycssloaders")
install.packages("shinyWidgets")
```
O `shinycssloaders` é um pacote mantido pelo [Dean Attali](https://github.com/daattali) que possui uma única função: `withSpinner()`. Para colocar a animação de carregamento em uma visualização, basta colocar a função `*Output()` dentro da função `withSpinner()`. Sempre que ela estiver sendo calculada, um *spinner* será mostrado no lugar.
Rode o Shiny app abaixo para ver um exemplo:
```{r, eval = FALSE}
library(shiny)
ui <- fluidPage(
titlePanel("Exemplo shinyWidgets::addSpinner"),
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "variavel",
label = "Escolha uma variável",
choices = names(mtcars)
)
),
mainPanel(
shinycssloaders::withSpinner(
plotOutput(outputId = "histograma"),
type = 4,
color = "orange",
size = 2
)
)
)
)
server <- function(input, output, session) {
output$histograma <- renderPlot({
Sys.sleep(5)
hist(mtcars[[input$variavel]])
})
}
shinyApp(ui, server)
```
Além várias opções de animações diferentes, que você pode trocar no argumento `type`, também é possível ajustar o tamanho, a cor, a cor de fundo e até usar uma imagem própria como animação^[Pode ser uma imagem estática ou GIF.].
O pacote `shinyWidgets` é mantido pela equipe da [dreamRs](https://github.com/dreamRs). Além de diversos widgets muito úteis, ele possui a função `shinyWidgets::addSpinner()`. Assim como a função `shinycssloards::withSpinner()`, basta embrulhar suas funções `*Output()` com a função `shinyWidgets::addSpinner()` para adicionar a animação às suas visualizações.
São diversas opções de animação, escolhidas por meio do argumento `spin`. Aqui você pode customizar apenas a cor delas. Rode o app a seguir para ver um exemplo.
```{r, eval = FALSE}
library(shiny)
ui <- fluidPage(
titlePanel("Exemplo shinyWidgets::addSpinner"),
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "variavel",
label = "Escolha uma variável",
choices = names(mtcars)
)
),
mainPanel(
shinyWidgets::addSpinner(
plotOutput(outputId = "histograma"),
spin = "cube",
color = "purple"
)
)
)
)
server <- function(input, output, session) {
output$histograma <- renderPlot({
Sys.sleep(5)
hist(mtcars[[input$variavel]])
})
}
shinyApp(ui, server)
```
## Barras de carregamento
## Usando Markdown para textos
É comum precisarmos colocar em nossos aplicativos páginas com orientações, informações adicionais ou referências.
Essas páginas geralmente são compostas por textos, links e imagens, facilmente produzidos em um arquivo Markdown. Construir esses elementos HTML dentro da UI dá bem mais trabalho, pois tudo precisa ser encapsulado por funções da lista `shiny::tags$`.
Por exemplo, vamos supor que queiramos colocar a seguinte *mini bio* em um app que estamos fazendo.
<br>
<center>
<img src="img/rick.jpeg" width="130px" style="border-radius: 65px;"/>
**Rick Sanchez**
Procurado por crimes contra o espaço interdimensional. Saiba mais [aqui](https://rickandmorty.fandom.com/wiki/Rick_Sanchez).
</center>
<br>
<br>
O arquivo Markdown para construir essa mini bio seria bem simples:
```
<center>
<img src="img/rick.jpeg" width="130px" style="border-radius: 65px;"/>
**Rick Sanchez**
Procurado por crimes contra o espaço interdimensional. Saiba mais [aqui](https://rickandmorty.fandom.com/wiki/Rick_Sanchez).
</center>
```
Construir a mesma minibio na UI é bem mais burocrático.
```{r, eval = FALSE}
fluidPage(
fluidRow(
column(
width = 12,
shiny::tags$img(
src = "img/rick.jpeg",
width = "130px",
style = "border-radius: 65px; display: block; margin: auto;"
),
shiny::tags$p(
shiny::tags$strong("Rick Sanchez"),
style = "text-align: center;"
),
shiny::tags$p(
style = "text-align: center;",
"Procurado por crimes contra o espaço interdimensional. Saiba mais",
shiny::tags$a(
href = "https://rickandmorty.fandom.com/wiki/Rick_Sanchez",
"aqui"
)
)
)
)
)
```
Mesmo um exemplo simples já começa a deixar claro o problema: produzir muitos elementos HTML na UI rapidamente transforma seu código em um emaranhado de funções aninhadas. O mesmo vale para textos muito grandes. Embora nesse caso nem sempre tenhamos muitas tags HTML para criar, scripts R não foram feitos para atender os cuidados que textos carecem.
A melhor prática nessas situações é justamente transferir esses elementos para um arquivo Markdown e pedir que o Shiny o transforme em HTML e o inclua no lugar adequado apenas na hora do `runApp()`. Para isso usamos a função `shiny::includeMarkdown()`.
Supondo que salvamos o markdown da mini bio em um arquivo `rick.md`, a nossa UI então ficaria:
```{r, eval=FALSE}
ui <- fluidPage(
fluidRow(
column(
width = 12,
includeMarkdown("rick.md")
)
)
)
```
Vale ressaltar que essa função compila arquivos Markdown (`.md`) e não R Markdown (`.Rmd`). Se quisermos rodar códigos R para gerar saídas HTML, devemos fazer isso dentro do próprio Shiny.