Depois de duas aulas com plotly e echarts4r, é tentador concluir que o assunto está resolvido. Não está — e a diferença entre um dashboard competente e um excelente muitas vezes está em escolher a ferramenta certa para um painel específico, em vez de espremer tudo na biblioteca que você já domina.
O ecossistema R de htmlwidgets é grande, e cada pacote resolve bem um problema estreito. Séries temporais longas pedem um comportamento de zoom que o plotly não entrega com elegância. Uma animação que revela evolução no tempo é natural no gganimate e trabalhosa em qualquer outro lugar. E há o caso extremo: quando nenhuma biblioteca faz o que você precisa, o r2d3 te dá D3 puro com um htmlwidget em volta.
Esta aula é um mapa dessas alternativas: o que cada uma faz melhor, quando vale a troca e — igualmente importante — quando não vale.
Conhecer as principais alternativas ao plotly no ecossistema R de widgets interativos, saber escolher entre elas por critério técnico em vez de hábito, e integrar as duas mais úteis no dia a dia — dygraphs para séries temporais e gganimate para animação — dentro do Shiny.
O critério de escolha, antes dos pacotes
Trocar de biblioteca tem custo real: mais uma dependência, mais uma API na cabeça do time, mais um jeito de capturar eventos. Só compensa quando o ganho é estrutural. Os três critérios que uso:
- O gesto do usuário é nativo na biblioteca? Arrastar para selecionar um intervalo de datas é nativo no
dygraphse improvisado noplotly. Se o gesto central do seu painel é nativo em outro pacote, troque. - O volume cabe? Bibliotecas que desenham em SVG (a maioria) engasgam entre 10 e 50 mil elementos. As que desenham em
canvasou WebGL aguentam ordens de grandeza mais. - Você precisa de eventos no Shiny? Nem todo widget publica eventos. Um gráfico lindo que não conversa com o servidor é, num dashboard reativo, um beco sem saída.
dygraphs: séries temporais levadas a sério
O dygraphs é a interface R para a biblioteca JavaScript de mesmo nome, e ela foi construída para uma coisa só: séries temporais. O que você ganha de graça:
- Seletor de intervalo (
dyRangeSelector) — a faixa embaixo do gráfico que arrasta e redimensiona; - Zoom por arrasto no eixo x, com duplo clique para voltar;
- Highlight sincronizado entre séries ao passar o mouse;
- Desempenho com dezenas de milhares de pontos, porque desenha em
canvas.
O preço: ele aceita séries temporais de verdade — um objeto xts ou algo conversível. Não é um gráfico genérico.
library(dygraphs)
library(xts)
# uma série diária de dois anos
datas <- seq(as.Date("2024-01-01"), as.Date("2025-12-31"), by = "day")
serie <- xts(
data.frame(
receita = cumsum(rnorm(length(datas), 50, 8)),
meta = seq(0, 30000, length.out = length(datas))
),
order.by = datas
)
dygraph(serie, main = "Receita acumulada vs. meta") |>
dySeries("receita", label = "Receita", color = "#2b6cb0") |>
dySeries("meta", label = "Meta", strokePattern = "dashed", color = "#a0aec0") |>
dyRangeSelector(height = 40) |>
dyOptions(fillGraph = TRUE, fillAlpha = 0.1) |>
dyHighlight(highlightSeriesOpts = list(strokeWidth = 3))
Capturando o intervalo escolhido no Shiny
O dygraphs publica o zoom em input$<id>_date_window, um vetor de duas datas em formato ISO. É o evento mais útil do pacote, porque permite que o resto do dashboard siga o recorte temporal que o usuário escolheu arrastando:
observeEvent(input$serie_date_window, {
janela <- input$serie_date_window
req(length(janela) == 2)
estado$inicio <- as.Date(substr(janela[[1]], 1, 10))
estado$fim <- as.Date(substr(janela[[2]], 1, 10))
})
Note o substr(): as datas chegam como strings ISO com hora e fuso ("2024-03-15T00:00:00.000Z"), e converter direto com as.Date() sobre o texto completo funciona, mas fica frágil se o fuso mudar. Cortar os 10 primeiros caracteres é explícito e previsível.
Mais uma vez, o padrão é o mesmo: o evento escreve no estado, os painéis leem do estado.
gganimate: quando o tempo é a mensagem
Há gráficos em que a evolução é o conteúdo — a clássica animação de renda por país ao longo de décadas comunica algo que um gráfico estático não comunica. O gganimate estende o ggplot2 com uma gramática de transição:
library(ggplot2)
library(gganimate)
p <- ggplot(dados, aes(x = pib_per_capita, y = expectativa, size = populacao, colour = continente)) +
geom_point(alpha = 0.7) +
scale_x_log10() +
labs(title = "Ano: {frame_time}", x = "PIB per capita", y = "Expectativa de vida") +
transition_time(ano) +
ease_aes("linear")
animate(p, width = 800, height = 450, fps = 12, duration = 12, renderer = gifski_renderer())
A parte que importa para um dashboard: isto não é um widget interativo, é um vídeo ou GIF. A renderização é cara — segundos a minutos — e acontece no servidor. Três consequências práticas:
- Nunca renderize dentro de um
reactiveque dispara a cada clique. Amarre a umactionButtoncomeventReactive, ou pré-renderize offline. - Use cache.
bindCache()sobre o render evita recalcular a mesma animação para os mesmos parâmetros. - Considere se a interatividade não serve melhor. Um
plotlycom umsliderInputde ano dá ao usuário controle sobre o tempo, o que costuma ser mais útil que uma animação que ele assiste passivamente. A animação brilha em apresentação e narrativa; o slider brilha em exploração.
output$animacao <- renderImage({
arquivo <- tempfile(fileext = ".gif")
anim_save(arquivo, animacao_calculada())
list(src = arquivo, contentType = "image/gif")
}, deleteFile = TRUE) |>
bindCache(input$recorte, input$metrica)
r2d3: a saída de emergência
Quando nenhum pacote faz o que você precisa — uma visualização genuinamente original, um layout de rede específico do seu domínio, uma metáfora visual que não existe pronta — o r2d3 embrulha D3.js num htmlwidget. Você escreve o D3 de verdade num arquivo .js e o R apenas entrega os dados e as opções.
library(r2d3)
r2d3(data = dados, script = "www/minha-viz.js", options = list(cor = "#2b6cb0"))
No lado JavaScript, os objetos data, options, width e height já estão disponíveis, e o widget redesenha quando os dados mudam.
Seja honesto sobre o custo: você está trocando "configurar uma biblioteca" por "programar uma visualização". Isso significa escrever escalas, eixos, legendas, tooltip e comportamento responsivo à mão — trabalho que um plotly resolve numa linha. Use quando o valor da visualização original justificar, e não porque a biblioteca pronta não ficou exatamente como o mockup pedia.
ggvis e rbokeh: o que a história ensina
Os dois aparecem em materiais de referência e merecem uma nota, porque saber o que não adotar também é conhecimento.
ggvisfoi uma tentativa (do próprio time doggplot2) de uma gramática de gráficos reativa e web-nativa. O projeto está parado há anos e o próprio site recomenda alternativas. Não inicie nada novo nele.rbokehé a ponte R para o Bokeh, do ecossistema Python. Foi removido do CRAN e a manutenção é esporádica.
A lição geral, que vale além desses dois: num htmlwidget, a manutenção importa mais que a elegância da API. O pacote carrega uma biblioteca JavaScript inteira para dentro do navegador do usuário; abandonado, ele acumula vulnerabilidades e quebra em navegadores novos. Antes de adotar um widget, olhe a data do último commit e a cadência de releases.
Um guia de decisão
| Situação | Escolha | Por quê |
|---|---|---|
| Série temporal longa, com zoom e seletor de intervalo | dygraphs | Gestos nativos, canvas, evento de janela pronto |
| Gráfico interativo de propósito geral | plotly | Cobertura e proxy maduros |
| Animação, sunburst, sankey, gauge, calendário | echarts4r | Repertório visual e animação nativa |
| Evolução temporal como narrativa | gganimate | Mas prefira slider + gráfico interativo para exploração |
| Mapa | leaflet | Aula dedicada |
| Rede / grafo | visNetwork | Aula dedicada |
| Visualização genuinamente original | r2d3 | Só quando o valor justificar o custo |
| Nada disso | Reveja o requisito | Frequentemente o pedido é uma tabela bem feita |
A última linha não é piada. Uma boa parte dos pedidos de "um gráfico bonito aqui" se resolve melhor com uma tabela ordenável e bem formatada.
Trocar de biblioteca custa dependência nova, API nova e mais um jeito de capturar eventos — só vale quando o ganho é estrutural: um gesto nativo, um volume de dados ou um evento de que você precisa. O dygraphs é especialista em série temporal, com dyRangeSelector, zoom por arrasto e canvas para dezenas de milhares de pontos; ele publica o recorte em input$<id>_date_window, e o jeito previsível de ler a data é cortar os dez primeiros caracteres. O gganimate produz GIF ou vídeo, não widget: a renderização é cara e acontece no servidor, então amarre a um botão, use bindCache() e considere se um slider com gráfico interativo não serviria melhor ao usuário. O r2d3 é a saída de emergência — D3 puro embrulhado, em que você troca "configurar" por "programar" e assume escalas, eixos e tooltip. Já ggvis e rbokeh estão parados e não devem começar projeto novo: num htmlwidget, manutenção pesa mais que elegância de API, porque ele injeta JavaScript no navegador de quem usa. E uma verdade que economiza trabalho: boa parte dos pedidos de "um gráfico aqui" se resolve melhor com uma tabela bem feita.
Exercícios
-
Um painel mostra 5 anos de cotação diária e o usuário precisa comparar dois trimestres arrastando. Qual biblioteca, e qual evento você usaria para propagar o recorte ao resto do dashboard?
✓ Resposta:dygraphs. O gesto de arrastar para delimitar um intervalo temporal é nativo nele (viadyRangeSelector()e o zoom por arrasto no eixo x), enquanto noplotlyseria improvisado a partir do evento derelayout. Além disso, 5 anos de cotação diária são ~1.300 pontos por série — confortável para ocanvasdodygraphs. Para propagar, leiainput$<id>_date_window, que traz as duas bordas como strings ISO; grave as datas no estado central (reactiveValues) e faça os demais painéis lerem dali. -
Seu colega colocou um
gganimatedentro de umrenderImageque depende de trêsselectInput. Os usuários reclamam que o app "trava". Diagnostique e proponha duas correções.✓ Resposta:O
gganimaterenderiza um GIF no servidor, o que leva de segundos a minutos. Ligado diretamente a três inputs reativos, ele re-renderiza a cada mexida em qualquer um deles, e como o R é single-threaded por sessão, a aplicação inteira fica bloqueada durante a renderização. Correções: (a) trocar a reatividade automática por umactionButton+eventReactive, para renderizar só quando o usuário pedir; (b) aplicarbindCache()com os três inputs como chave, para que combinações já vistas retornem instantaneamente. Uma terceira via, muitas vezes a melhor: substituir a animação por um gráfico interativo comsliderInputde tempo, que dá controle ao usuário e custa uma fração do processamento. -
Por que a recomendação contra
ggviserbokehse apoia em manutenção e não em qualidade da API?✓ Resposta:Porque um
htmlwidgetnão é apenas código R: ele injeta uma biblioteca JavaScript no navegador do usuário. Um pacote sem manutenção significa JavaScript sem correção de segurança, que acumula vulnerabilidades conhecidas, e sem adaptação a mudanças de navegadores — quebrando silenciosamente com o tempo. Uma API elegante não protege contra nada disso. Por isso o critério prático antes de adotar um widget é olhar a data do último commit e a cadência de releases, antes de olhar a sintaxe. -
Quando o
r2d3é a escolha certa, e qual custo você precisa aceitar conscientemente?✓ Resposta:Quando a visualização é genuinamente original — uma metáfora visual que não existe pronta em nenhuma biblioteca e cujo valor comunicativo justifica o investimento. O custo é que você deixa de configurar uma visualização e passa a programá-la: escalas, eixos, legendas, tooltip, transições e comportamento responsivo passam a ser código seu, em JavaScript, com manutenção sua. O erro comum é recorrer ao
r2d3porque a biblioteca pronta não ficou pixel-a-pixel igual ao mockup — quase sempre ajustar o mockup sai muito mais barato. -
Um gerente pede "um gráfico bonito" para mostrar 12 indicadores com valor atual, meta e variação. Por que uma tabela pode ser a resposta melhor?
✓ Resposta:Porque o pedido não tem uma relação para visualizar — não há tendência, distribuição, correlação ou composição. São 12 leituras pontuais com três atributos cada. Gráfico serve para revelar padrão em um conjunto; aqui o usuário quer ler valores exatos e comparar com a meta, tarefa em que a tabela é objetivamente superior: valor preciso, alinhamento que facilita a comparação vertical, ordenação por qualquer coluna. Uma tabela bem formatada — com destaque condicional de quem está abaixo da meta e um minigráfico de tendência por linha — comunica melhor e custa menos.