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 dygraphs e improvisado no plotly. 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 canvas ou 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:

  1. Nunca renderize dentro de um reactive que dispara a cada clique. Amarre a um actionButton com eventReactive, ou pré-renderize offline.
  2. Use cache. bindCache() sobre o render evita recalcular a mesma animação para os mesmos parâmetros.
  3. Considere se a interatividade não serve melhor. Um plotly com um sliderInput de 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.

  • ggvis foi uma tentativa (do próprio time do ggplot2) 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 intervalodygraphsGestos nativos, canvas, evento de janela pronto
Gráfico interativo de propósito geralplotlyCobertura e proxy maduros
Animação, sunburst, sankey, gauge, calendárioecharts4rRepertório visual e animação nativa
Evolução temporal como narrativagganimateMas prefira slider + gráfico interativo para exploração
MapaleafletAula dedicada
Rede / grafovisNetworkAula dedicada
Visualização genuinamente originalr2d3Só quando o valor justificar o custo
Nada dissoReveja o requisitoFrequentemente 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

  1. 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 (via dyRangeSelector() e o zoom por arrasto no eixo x), enquanto no plotly seria improvisado a partir do evento de relayout. Além disso, 5 anos de cotação diária são ~1.300 pontos por série — confortável para o canvas do dygraphs. Para propagar, leia input$<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.

  2. Seu colega colocou um gganimate dentro de um renderImage que depende de três selectInput. Os usuários reclamam que o app "trava". Diagnostique e proponha duas correções.

    ✓ Resposta:

    O gganimate renderiza 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 um actionButton + eventReactive, para renderizar só quando o usuário pedir; (b) aplicar bindCache() 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 com sliderInput de tempo, que dá controle ao usuário e custa uma fração do processamento.

  3. Por que a recomendação contra ggvis e rbokeh se apoia em manutenção e não em qualidade da API?

    ✓ Resposta:

    Porque um htmlwidget nã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.

  4. 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 r2d3 porque a biblioteca pronta não ficou pixel-a-pixel igual ao mockup — quase sempre ajustar o mockup sai muito mais barato.

  5. 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.

Fontes e leituras recomendadas