Crie seletores de visualização no Shiny

Neste post, vamos mostrar como usar o widget radioGroupButtons do pacote shinyWidgets para construir um app com seletor de visualizações, isto é, botões que mudam o tipo de visualização apresentada na tela.

Como exemplo, vamos construir um shiny app que mostra um gráfico de barras, um gráfico de linhas ou uma tabela (a depender da escolha da pessoa que estiver usando) do número de gols do Brasileirão1 por temporada.

O primeiro passo é configurar o widget corretamente. Veja abaixo que o segredo é utilizar os argumentos choiceValues e choiceNames. Para o primeiro, passamos os valores que serão acessados dentro do server a depender da escolha na UI (equivalente ao argumento choices). Ao segundo, podemos passar tanto textos (que seriam escritos dentro do botão) quanto ícones. Nesse caso, utilizamos ícones da biblioteca Font Awesome, com ajuda da função shiny::icon(). Repare também que deixamos o tamanho dos botões um pouco maiores setando o argumento size = "lg.

shinyWidgets::radioGroupButtons(
  inputId = "vis_escolhida",
  label = "",
  choiceValues = c("barras", "linhas", "tabela"),
  choiceNames = list(
    icon("bar-chart"),
    icon("line-chart"),
    icon("table")
  ),
  size = "lg",
  selected = "barras"
)

Agora, precisamos construir a lógica do Output, tanto na UI como no server. Como a nossa visualização pode gerar gráficos ou uma tabela, precisaremos de funções *Output() e render*() diferentes. Dessa forma, vamos utilizar na nossa UI um OutputUI().

# A nossa UI ficará assim
ui <- fluidPage(
  fluidRow(
    column(
      width = 12,
      h1("App com seletor de visualizações")
    )
  ),
  br(),
  fluidRow(
    column(
      offset = 1,
      width = 11,
      shinyWidgets::radioGroupButtons(
        inputId = "vis_escolhida",
        label = "",
        choiceValues = c("barras", "linhas", "tabela"),
        choiceNames = list(
          icon("bar-chart"),
          icon("line-chart"),
          icon("table")
        ),
        size = "lg",
        selected = "barras"
      )
    )
  ),
  br(),
  fluidRow(
    column(
      width = 12,
      uiOutput("vis")
    )
  )
)

Agora, no server, podemos criar funções *Output() diferentes a depender da visualização escolhida. Usamos plotOutput() para os gráficos e reactable::reactableOutput() para a tabela.

output$vis <- renderUI({
  if (input$vis_escolhida %in% c("barras", "linhas")) {
    plotOutput("grafico")
  } else if (input$vis_escolhida == "tabela") {
    reactable::reactableOutput("tabela")
  }
})

Agora vamos construir nossas visualizações. Primeiro, vamos montar a base que precisamos para gerar tanto os gráficos quanto a tabela.

Os dados vêm do pacote brasileirao. Se você não possui esse pacote instalado, basta rodar o código abaixo:

remotes::install_github("williamorim/brasileirao")

Como a tabela que gera os gráficos não depende de nenhum valor ou expressão reativa, podemos colocar o código que a gera diretamente no server.

tab <- brasileirao::matches |>
  dplyr::filter(
    score != "x",
    season %in% 2006:2020
  ) |>
  tidyr::separate(
    score,
    c("gols_casa", "gols_visitante"),
    sep = "x",
    convert = TRUE
  ) |>
  dplyr::mutate(
    gols = gols_casa + gols_visitante
  ) |>
  dplyr::group_by(season) |>
  dplyr::summarise(gols = sum(gols))

Para gerar os gráficos, só precisamos de um renderPlot() e um if/else para devolver o gráfico certo.

output$grafico <- renderPlot({
  
  grafico_base <- tab |>
    ggplot(aes(x = season, y = gols)) +
    labs(x = "Temporada", y = "Número de gols") +
    theme_minimal() +
    ggtitle("Número de gols do Brasileirão por temporada")
  
  if (input$vis_escolhida == "linhas") {
    grafico_base +
      geom_line(color = "dark green")
  } else if (input$vis_escolhida == "barras") {
    grafico_base +
      geom_col(width = 0.5, fill = "dark green")
  }
})

Para gerar a tabela, precisamos apenas de um renderReactable().

output$tabela <- reactable::renderReactable({
  tab |>
    reactable::reactable(
      fullWidth = FALSE,
      columns = list(
        season = reactable::colDef(
          name = "Temporada"
        ),
        gols = reactable::colDef(
          name = "Número de gols"
        )
      )
    )
})

Juntando tudo, temos o app a seguir:

library(shiny)
library(ggplot2)

ui <- fluidPage(
  fluidRow(
    column(
      width = 12,
      h1("App com seletor de visualizações")
    )
  ),
  br(),
  fluidRow(
    column(
      offset = 1,
      width = 11,
      shinyWidgets::radioGroupButtons(
        inputId = "vis_escolhida",
        label = "",
        choiceValues = c("barras", "linhas", "tabela"),
        choiceNames = list(
          icon("bar-chart"),
          icon("line-chart"),
          icon("table")
        ),
        size = "lg",
        selected = "barras"
      )
    )
  ),
  br(),
  fluidRow(
    column(
      width = 12,
      uiOutput("vis")
    )
  )
)

server <- function(input, output, session) {

  output$vis <- renderUI({
    if (input$vis_escolhida %in% c("barras", "linhas")) {
      plotOutput("grafico")
    } else if (input$vis_escolhida == "tabela") {
      reactable::reactableOutput("tabela")
    }
  })

  tab <- brasileirao::matches |>
    dplyr::filter(
      score != "x",
      season %in% 2006:2020
    ) |>
    tidyr::separate(
      score,
      c("gols_casa", "gols_visitante"),
      sep = "x",
      convert = TRUE
    ) |>
    dplyr::mutate(
      gols = gols_casa + gols_visitante
    ) |>
    dplyr::group_by(season) |>
    dplyr::summarise(gols = sum(gols))

  output$grafico <- renderPlot({

    grafico_base <- tab |>
      ggplot(aes(x = season, y = gols)) +
      labs(x = "Temporada", y = "Número de gols") +
      theme_minimal() +
      ggtitle("Número de gols do Brasileirão por temporada")

    if (input$vis_escolhida == "linhas") {
      grafico_base +
        geom_line(color = "dark green")
    } else if (input$vis_escolhida == "barras") {
      grafico_base +
        geom_col(width = 0.5, fill = "dark green")
    }
  })

  output$tabela <- reactable::renderReactable({
    tab |>
      reactable::reactable(
        fullWidth = FALSE,
        columns = list(
          season = reactable::colDef(
            name = "Temporada"
          ),
          gols = reactable::colDef(
            name = "Número de gols"
          )
        )
      )
  })
}

shinyApp(ui, server)

É isso! Dúvidas, sugestões e críticas, mande aqui nos comentários.

Até a próxima!


  1. Campeonato Brasileiro de futebol da Série A.↩︎

comments powered by Disqus