Você já tem o plotly funcionando e sincronizado. Então por que aprender outra biblioteca de gráficos? Porque elas resolvem problemas diferentes. O plotly é o canivete suíço — cobre quase tudo, tem um proxy poderoso e uma API estável. O echarts4r é a interface R para o Apache ECharts, e traz três coisas que o plotly não entrega com a mesma facilidade: animações e transições nativas, um repertório visual maior (sunburst, sankey, gauge, calendário, grafos) e um sistema de tooltip que é, na prática, um pequeno mecanismo de template HTML. Num dashboard executivo, é frequentemente o tooltip que separa "um gráfico" de "um painel que a diretoria entende sozinha".
Esta aula é sobre integrar o echarts4r ao Shiny de verdade: capturar os eventos de mouse que ele devolve, sincronizar dois gráficos, e construir tooltips que mostram muito mais que um par de números.
Dominar a integração echarts4r + Shiny: conhecer o conjunto completo de eventos que o pacote publica em input$, sincronizar visualizações por esses eventos, escrever tooltips personalizadas com formatter em JavaScript e com templates de string em R, e saber onde o proxy do echarts4r ajuda — e onde ele decepciona.
A gramática: e_charts() e o encadeamento
O echarts4r segue a lógica do pipe, como o ggplot2 segue a do +. Começa-se declarando o eixo x com e_charts() e, a partir daí, cada camada é uma função e_*():
library(echarts4r)
library(dplyr)
vendas <- data.frame(
mes = factor(month.abb[1:6], levels = month.abb[1:6]),
receita = c(120, 145, 132, 178, 165, 199),
custo = c( 90, 102, 98, 120, 118, 131)
)
vendas |>
e_charts(mes) |>
e_bar(receita, name = "Receita") |>
e_line(custo, name = "Custo") |>
e_tooltip(trigger = "axis") |>
e_title("Desempenho do semestre")
Duas diferenças importantes em relação ao plotly:
- Agrupamento vira série. Um
group_by()antes doe_charts()faz oecharts4rcriar uma série por grupo automaticamente. É o caminho natural para gráficos multi-série. trigger = "axis"vs"item". No modoaxis, passar o mouse em qualquer ponto da vertical mostra todas as séries daquele x — é o que se espera num gráfico temporal. No modoitem(padrão de dispersões), mostra só o ponto sob o cursor.
O contrato de eventos com o Shiny
Aqui está o ponto que economiza horas: o echarts4r publica os eventos como inputs do Shiny cujo nome é o outputId do gráfico mais um sufixo fixo. Se o seu gráfico é echarts4rOutput("vendas"), você lê:
| input | o que traz |
|---|---|
input$vendas_clicked_data | o dado completo do ponto clicado (lista) |
input$vendas_clicked_data_value | só o valor do ponto clicado |
input$vendas_clicked_row | o número da linha clicada |
input$vendas_clicked_serie | o nome da série clicada |
input$vendas_mouseover_data | idem, para o ponto sob o cursor |
input$vendas_mouseover_data_value | o valor sob o cursor |
input$vendas_mouseover_row | a linha sob o cursor |
input$vendas_mouseover_serie | a série sob o cursor |
input$vendas_brush | os pontos selecionados por área (brush) |
input$vendas_legend_change | o nome da série que teve a legenda alternada |
input$vendas_legend_selected | o estado (ligado/desligado) de todas as séries |
Compare com o plotly, que centraliza tudo em event_data("plotly_click", source = "..."). O echarts4r não precisa de source: o próprio outputId já faz esse papel. É mais simples de ler e mais difícil de confundir quando há muitos gráficos.
Um detalhe que causa erro na primeira execução: antes da primeira interação, esses inputs são NULL. Todo observeEvent/reactive que os consome precisa de req().
Clique, hover e brush na prática
library(shiny)
library(bslib)
library(echarts4r)
library(dplyr)
tb <- mtcars |>
tibble::rownames_to_column("modelo") |>
rename(peso = wt, consumo = mpg)
ui <- page_sidebar(
title = "Eventos do echarts4r",
sidebar = sidebar(
width = 340,
h6("Clique"), verbatimTextOutput("log_clique"),
h6("Hover"), verbatimTextOutput("log_hover"),
h6("Seleção"), verbatimTextOutput("log_brush")
),
card(echarts4rOutput("dispersao", height = 460))
)
server <- function(input, output, session) {
output$dispersao <- renderEcharts4r({
tb |>
e_charts(peso) |>
e_scatter(consumo, bind = modelo, symbol_size = 14) |>
e_brush() |> # habilita a seleção por área
e_tooltip() |>
e_x_axis(scale = TRUE) |>
e_y_axis(scale = TRUE)
})
output$log_clique <- renderPrint({
req(input$dispersao_clicked_data)
str(input$dispersao_clicked_data)
})
output$log_hover <- renderPrint({
req(input$dispersao_mouseover_row)
tb[input$dispersao_mouseover_row, c("modelo", "peso", "consumo")]
})
output$log_brush <- renderPrint({
req(input$dispersao_brush)
input$dispersao_brush
})
}
shinyApp(ui, server)
Três observações que valem mais que o código:
bind = modeloé okeydoecharts4r. Sem ele, o clique devolve coordenadas e você não sabe qual registro foi clicado. Com ele, o identificador viaja junto e chega emparams.nameno tooltip e no evento.e_brush()precisa ser declarado. Ao contrário doplotly, a seleção por área não vem ligada por padrão._clicked_rowé ouro. Ele devolve o índice da linha no data frame original — o caminho mais curto para ir do clique de volta aos seus dados, sem parsing de strings.
A legenda como filtro de verdade
Num echarts4r multi-série, clicar na legenda liga e desliga séries. Isso é visual por padrão, mas os eventos de legenda permitem transformar esse gesto em filtro de aplicação: quando o usuário desliga "Região Sul", você pode recalcular uma tabela, um KPI ou uma consulta.
observeEvent(input$vendas_legend_change, {
serie <- input$vendas_legend_change
estado <- input$vendas_legend_selected # lista nomeada: TRUE/FALSE por série
visiveis <- names(estado)[unlist(estado)]
estado_app$series_ativas <- visiveis # escreve no estado central
})
Repare que isto é exatamente o padrão "interações escrevem no estado, painéis leem do estado" — só muda a origem do evento. É por isso que vale a pena ter o estado centralizado: trocar plotly por echarts4r num painel não obriga a reescrever os outros.
Sincronizando dois gráficos
Há dois caminhos, e a escolha segue a mesma lógica cliente/servidor.
No cliente, com e_connect() — o mais barato. Dois gráficos conectados compartilham tooltip e destaque, sem passar pelo servidor:
g1 <- vendas |> e_charts(mes, elementId = "g1") |> e_line(receita) |> e_tooltip(trigger = "axis")
g2 <- vendas |> e_charts(mes, elementId = "g2") |> e_bar(custo) |> e_tooltip(trigger = "axis")
g1 |> e_connect("g2") # passar o mouse num destaca o ponto correspondente no outro
O e_connect() exige elementId explícito nos gráficos e funciona só para tooltip e destaque — ele não filtra nem recalcula nada.
No servidor, via evento — quando o gesto num gráfico precisa mudar o conteúdo do outro:
selecionada <- reactiveVal(NULL)
observeEvent(input$categorias_clicked_data, {
selecionada(input$categorias_clicked_data$name)
})
output$detalhe <- renderEcharts4r({
req(selecionada())
dados |>
filter(categoria == selecionada()) |>
group_by(mes) |>
summarise(total = sum(valor)) |>
e_charts(mes) |>
e_line(total) |>
e_title(paste("Evolução —", selecionada()))
})
Tooltips personalizadas: o formatter
O tooltip padrão mostra o que o ECharts acha relevante. Quase nunca é o que você quer. O controle vem do argumento formatter de e_tooltip(), e há dois caminhos.
Caminho A — JavaScript, com htmlwidgets::JS(). O ECharts entrega um objeto params para a função, e você monta a string de retorno:
tb |>
e_charts(peso) |>
e_scatter(consumo, qsec, bind = modelo) |>
e_tooltip(formatter = htmlwidgets::JS("
function(params) {
return '<strong>' + params.name + '</strong><br/>' +
'Peso: ' + params.value[0] + '<br/>' +
'Consumo: ' + params.value[1] + '<br/>' +
'Aceleração: ' + params.value[2];
}
"))
Os campos úteis de params: name (o que veio no bind), value (vetor com as dimensões na ordem em que foram passadas), seriesName e dataIndex.
Caminho B — montar o HTML em R, e deixar o JS só devolvê-lo. É o truque que evita escrever JavaScript:
library(glue)
tb |>
mutate(cartao = glue(
"<strong>{modelo}</strong><br/>Peso: {peso} t<br/>Consumo: {consumo} mpg"
)) |>
e_charts(peso) |>
e_scatter(consumo, bind = cartao) |>
e_tooltip(formatter = htmlwidgets::JS("function(params){ return params.name; }"))
O bind passa a carregar o HTML pronto, e o formatter vira uma linha só. A vantagem prática é grande: toda a formatação — format(), scales::percent(), condicionais — acontece em R, onde você já sabe trabalhar, e fica testável.
Tooltip rica: um cartão com imagem
Levando o caminho B ao limite, o tooltip pode ser um cartão completo. É o recurso que transforma um gráfico num painel de consulta:
produtos <- produtos |>
mutate(cartao = glue('
<div style="width:240px;font-family:system-ui,sans-serif">
<div style="display:flex;gap:10px;align-items:center">
<img src="{url_foto}" style="width:56px;height:56px;border-radius:8px;object-fit:cover">
<div>
<div style="font-weight:700;font-size:14px">{nome}</div>
<div style="font-size:12px;color:#5f6b7a">{categoria}</div>
</div>
</div>
<div style="margin-top:8px;font-size:13px">
Preço: <b>{scales::dollar(preco, prefix = "R$ ")}</b><br>
Estoque: <b>{estoque}</b> un.
</div>
</div>
'))
Duas armadilhas em produção:
- As imagens precisam ser acessíveis pelo navegador. Um caminho de arquivo do servidor não funciona; use
addResourcePath()para publicar a pasta, ou dados embutidos embase64. - Escape. Se
nomevier do banco e contiver<ou&, o cartão quebra ou vira um vetor de injeção. Passe porhtmltools::htmlEscape()antes doglue().
echarts4rProxy: expectativas calibradas
O echarts4r tem proxy — echarts4rProxy() com e_append*_p(), e_highlight_p(), e_focus_adjacency_p() — e ele serve bem para acrescentar dados a uma série existente e para destacar elementos sem redesenhar.
O que ele não faz bem, e é justo dizer: comparado ao plotlyProxy ou ao leafletProxy, a cobertura é menor. Mudar a escala de um eixo, trocar o conjunto de dados inteiro ou reconfigurar a legenda normalmente exige recriar o gráfico no renderEcharts4r. A regra prática: se a mudança é incremental (um ponto novo, um destaque), use proxy; se é estrutural, redesenhe e aceite o custo.
observeEvent(input$novo_ponto, {
echarts4rProxy("serie_tempo") |>
e_append1_p(serie_index = 0, x = Sys.time(), y = runif(1, 80, 120)) |>
e_execute() # não esqueça: sem e_execute() nada acontece
})
Esquecer o e_execute() no fim da cadeia é o erro mais comum com o proxy do echarts4r — o código roda, não dá erro, e nada muda na tela.
O echarts4r complementa o plotly com mais tipos de gráfico, animações nativas e tooltips bem mais flexíveis. Os eventos chegam ao Shiny no formato input$<outputId>_<sufixo> — _clicked_data, _clicked_row, _clicked_serie, _mouseover_*, _brush, _legend_change, _legend_selected —, e aqui não existe source: o próprio outputId identifica o gráfico. O bind = faz o papel do key, e sem ele o evento não diz qual registro foi tocado; o _clicked_row devolve o índice direto no data frame. O e_brush() precisa ser habilitado explicitamente, e como os inputs de evento são NULL antes da primeira interação, req() sempre. Para sincronizar, e_connect() resolve no cliente — só tooltip e destaque, e exige elementId —, enquanto evento mais estado central resolve no servidor, quando é preciso recalcular. No tooltip, dá para usar formatter com htmlwidgets::JS(), mas monta-se melhor o HTML em R com glue(), deixando o JS só devolver params.name — e escapando o que vier do banco. Por fim, o echarts4rProxy() serve para acrescentar e destacar; mudança estrutural pede redesenho, e sem e_execute() o proxy não faz nada.
Exercícios
-
No
plotlyvocê precisa desource = "mapa"para distinguir eventos de gráficos diferentes. Por que oecharts4rnão precisa de nada equivalente?✓ Resposta:Porque o
echarts4rderiva o nome do input do própriooutputId. Um gráfico emecharts4rOutput("mapa")publicainput$mapa_clicked_data, e um emecharts4rOutput("ranking")publicainput$ranking_clicked_data— os namespaces já são distintos por construção. Noplotly, todos os eventos passam pela função únicaevent_data(), então é preciso um rótulo (source) para dizer de qual gráfico se quer ler; sem ele, os eventos de vários gráficos se misturam. -
Você clicou num ponto e quer recuperar a linha completa do seu data frame, com todas as colunas — inclusive as que não foram plotadas. Qual evento usar, e por quê?
✓ Resposta:input$<id>_clicked_row. Ele devolve o índice da linha no data frame que alimentou o gráfico, então bastadados[input$graf_clicked_row, ]para ter o registro inteiro, com colunas que nunca foram desenhadas. As alternativas são piores:_clicked_data_valuetraz só os valores das dimensões plotadas, e_clicked_datatraz a estrutura do ponto — ambos obrigariam a uma busca por valor, que é frágil quando há empates. -
Um colega usou
e_connect()esperando que, ao filtrar por seleção num gráfico, o outro fosse recalculado. Não funcionou. Explique e proponha a correção.✓ Resposta:O
e_connect()é um recurso de cliente e puramente visual: ele sincroniza tooltip e destaque entre gráficos, nada mais. Ele não filtra dados, não agrega e não fala com o servidor. Para recalcular, é preciso ir pelo servidor: capturar o gesto comobserveEvent(input$graf_clicked_data, ...)(ou_brush), gravar a seleção numreactiveVal/reactiveValues, e fazer o segundo gráfico ser umrenderEcharts4rque lê esse estado, filtra os dados e redesenha. -
Qual a vantagem prática de montar o HTML do tooltip em R com
glue()e usarformatter = JS("function(params){ return params.name; }"), em vez de escrever toda a lógica em JavaScript?✓ Resposta:Toda a formatação fica em R, onde você já domina as ferramentas:
scales::percent(),format(),ifelse()para regras condicionais, e pode inclusive testar o texto gerado com um teste unitário comum. O JavaScript fica reduzido a uma linha que nunca muda. Além disso, evita o problema de passar dados de R para dentro de uma string de JS (aspas, escapes, tipos), que é uma fonte recorrente de bugs difíceis de depurar no console do navegador. -
Você adicionou
echarts4rProxy("serie") |> e_append1_p(...)numobserveEvente o gráfico não muda — sem nenhum erro no console. Qual a causa mais provável?✓ Resposta:Falta o
e_execute()no fim da cadeia. As funções*_p()do proxy apenas acumulam as operações; é oe_execute()que as envia ao widget no navegador. Como a cadeia é sintaticamente válida sem ele, nada falha — o código simplesmente não produz efeito, o que torna o problema especialmente enganoso. Se oe_execute()já estiver lá, as próximas suspeitas são ooutputIderrado noecharts4rProxy()e umserie_indexinexistente (ele é baseado em zero).