Um dashboard profissional raramente tem um gráfico. Tem vários — um mapa, uma série temporal, um ranking, uma tabela — e o valor está em fazê-los conversar: clicar numa barra filtra o mapa; selecionar uma região destaca os pontos correspondentes na dispersão; passar o mouse numa linha realça a mesma categoria em todos os painéis. Este módulo é sobre construir essa orquestração de duas formas complementares: no cliente (com crosstalk, sem ida ao servidor) e no servidor (com o grafo reativo e eventos de plotly).
Dominar a sincronização de múltiplas visualizações: linkar seleção e filtro entre widgets com crosstalk; capturar eventos de clique, hover e seleção com plotly via event_data(); e coordenar vários outputs pelo servidor usando reactive/reactiveValues, sabendo escolher entre a abordagem cliente e a servidor.
Duas filosofias de sincronização
Há duas maneiras de fazer visualizações se sincronizarem, e escolher errado custa performance ou flexibilidade:
- Sincronização no cliente (
crosstalk): os widgets compartilham um objeto de dados no navegador (SharedData). Quando você seleciona pontos num, os outros reagem instantaneamente, sem ir ao servidor. Ideal para brushing/linking rápido sobre um dataset que cabe no navegador (milhares, não milhões, de linhas). Limitação: só funciona entre widgets compatíveis com crosstalk (plotly,leaflet,DT,d3scatter) e apenas para seleção/filtro, não para transformações arbitrárias. - Sincronização no servidor (reatividade): cada clique vira um
input, o servidor recalcula, e os outputs se redesenham. Mais poderosa (pode fazer qualquer transformação, agregação, consulta a banco), porém com a latência de um ciclo cliente→servidor→cliente.
Regra prática: filtro/seleção visual e leve → cliente (crosstalk); recalcular, agregar, consultar banco, mudar de dataset → servidor. Dashboards reais frequentemente combinam as duas.
Sincronização no cliente com crosstalk
O crosstalk envolve seu data frame num objeto SharedData. Qualquer widget compatível construído sobre esse objeto passa a compartilhar seleção e filtro.
library(crosstalk)
library(plotly)
library(DT)
# Um único SharedData alimenta todos os widgets.
# 'key' identifica cada linha unicamente entre os widgets.
sd <- SharedData$new(iris, key = ~row.names(iris))
# Dispersão que EMITE e RECEBE seleção
p <- plot_ly(sd, x = ~Sepal.Length, y = ~Petal.Length,
color = ~Species, type = "scatter", mode = "markers") |>
highlight(on = "plotly_selected", off = "plotly_deselect")
# Tabela que reflete a mesma seleção
tab <- datatable(sd, width = "100%")
# bscols dispõe lado a lado (funciona em .Rmd, Quarto e Shiny)
bscols(widths = c(6, 6), p, tab)
Ao selecionar pontos no gráfico (com a ferramenta de laço), as linhas correspondentes se destacam na tabela — sem uma linha de código de servidor. O segredo é o key: ele une as mesmas observações entre widgets.
Você também pode adicionar filtros que operam no cliente:
library(crosstalk)
sd <- SharedData$new(mtcars)
bscols(
widths = c(3, 9),
list(
filter_slider("hp", "Potência (hp)", sd, ~hp),
filter_checkbox("cyl", "Cilindros", sd, ~cyl, inline = TRUE)
),
plot_ly(sd, x = ~wt, y = ~mpg, type = "scatter", mode = "markers")
)
Os filter_* do crosstalk filtram o SharedData no navegador; o gráfico reage na hora. Perfeito para relatórios Quarto/flexdashboard sem servidor Shiny.
Controlando o destaque (highlight)
plot_ly(sd, x = ~x, y = ~y) |>
highlight(
on = "plotly_hover", # o que ATIVA o destaque
off = "plotly_doubleclick",# o que LIMPA
color = "red", # cor do destaque
selectize = TRUE, # caixa de busca para selecionar por valor
dynamic = TRUE, # permite escolher a cor dinamicamente
persistent = TRUE # acumula seleções em vez de substituir
)
Sincronização no servidor: eventos de plotly
Quando você precisa de mais do que destacar/filtrar — recalcular uma agregação, consultar um banco, mudar completamente outro painel — a sincronização vai para o servidor. O plotly no Shiny expõe interações do usuário como eventos que você lê com event_data().
Os quatro eventos mais úteis:
plotly_click— clique num ponto/barra.plotly_hover— mouse sobre um elemento.plotly_selected— seleção por caixa/laço (retorna vários pontos).plotly_relayout— zoom/pan (retorna a nova janela de eixos).
library(shiny)
library(plotly)
library(dplyr)
ui <- fluidPage(
plotlyOutput("barras"),
plotlyOutput("detalhe"),
verbatimTextOutput("info")
)
server <- function(input, output, session) {
# Gráfico mestre: vendas por categoria
vendas <- reactive({
data.frame(
categoria = c("A", "B", "C", "D"),
total = c(120, 90, 150, 60)
)
})
output$barras <- renderPlotly({
plot_ly(vendas(), x = ~categoria, y = ~total, type = "bar",
source = "src_barras", # nomeia a fonte de eventos
key = ~categoria) |> # 'key' identifica o clicado
layout(title = "Clique numa categoria")
})
# Lê o clique no gráfico de barras
categoria_sel <- reactive({
ev <- event_data("plotly_click", source = "src_barras")
req(ev) # nada selecionado ainda → para
ev$key[[1]] # a categoria clicada
})
# Gráfico detalhe: reage à categoria clicada (recalcula no servidor)
output$detalhe <- renderPlotly({
cat <- categoria_sel()
detalhe <- data.frame(mes = month.abb[1:6],
valor = round(runif(6, 10, 40)))
plot_ly(detalhe, x = ~mes, y = ~valor, type = "scatter", mode = "lines+markers") |>
layout(title = paste("Detalhe de", cat))
})
output$info <- renderPrint({
cat("Categoria selecionada:", categoria_sel())
})
}
shinyApp(ui, server)
Pontos-chave desse padrão:
source = "..."nomeia a fonte de eventos, para queevent_data()saiba de qual gráfico ler (essencial quando há váriosplotlyna tela).key = ~colunacarrega a identidade do elemento clicado até o servidor.req(ev)impede erro antes do primeiro clique.- O gráfico de detalhe é um
reactive/rendercomum que depende decategoria_sel()— ou seja, toda a maquinaria da arquitetura reativa se aplica.
registerPlotlyEvents e priorizar eventos
Por padrão, alguns eventos (como plotly_hover) podem não estar "ligados". Você garante o registro configurando o gráfico e, quando o disparo precisa ser imediato/ignorar duplicatas, usa event_register:
output$g <- renderPlotly({
plot_ly(df, x = ~x, y = ~y, source = "s") |>
event_register("plotly_hover") # garante que hover seja capturável
})
hover <- reactive({
event_data("plotly_hover", source = "s", priority = "event")
# priority = "event" dispara mesmo que o valor seja igual ao anterior
})
Coordenando vários painéis por um estado central
Quando três ou mais painéis interagem, espalhar event_data por todo lado vira espaguete. O padrão limpo é ter um estado central em reactiveValues que cada interação atualiza e cada painel lê. É o mesmo padrão de estado da arquitetura reativa, agora aplicado a visualização.
server <- function(input, output, session) {
# ESTADO CENTRAL do dashboard
estado <- reactiveValues(
regiao = NULL,
periodo = c(2020, 2024),
metrica = "receita"
)
# Cada interação ATUALIZA o estado (efeito colateral → observeEvent)
observeEvent(event_data("plotly_click", source = "mapa"), {
estado$regiao <- event_data("plotly_click", source = "mapa")$key
})
observeEvent(input$metrica, estado$metrica <- input$metrica)
observeEvent(input$periodo, estado$periodo <- input$periodo)
# Um único reactive de dados derivado do estado (compartilhado, cacheado)
dados_filtrados <- reactive({
df |>
filter(is.null(estado$regiao) | regiao == estado$regiao,
ano >= estado$periodo[1], ano <= estado$periodo[2])
})
# Cada painel apenas LÊ dados_filtrados() — nenhum sabe dos outros
output$serie <- renderPlotly({ plot_ly(dados_filtrados(), x = ~ano, y = ~get(estado$metrica)) })
output$ranking <- renderPlotly({ plot_ly(dados_filtrados() |> count(categoria), x = ~n, y = ~categoria, type = "bar") })
output$tabela <- DT::renderDT({ dados_filtrados() })
}
Por que isso é bom: cada painel depende de uma fonte de verdade (dados_filtrados()), que por sua vez deriva do estado. Adicionar um quarto painel não toca nos outros três. Esse desenho — interações escrevem no estado; painéis leem do estado — é o que torna dashboards complexos sustentáveis, e reaparece na arquitetura modular (programação em Shiny).
Sincronizando eixos e zoom (plotly_relayout)
Um pedido comum: dois gráficos empilhados que devem compartilhar o mesmo zoom no eixo X (ex.: preço e volume de uma ação). Captura-se o plotly_relayout de um e aplica-se ao outro via plotlyProxy (atualiza sem redesenhar tudo).
observeEvent(event_data("plotly_relayout", source = "preco"), {
rl <- event_data("plotly_relayout", source = "preco")
# rl traz xaxis.range[0] e xaxis.range[1] quando há zoom
if (!is.null(rl[["xaxis.range[0]"]])) {
plotlyProxy("volume", session) |>
plotlyProxyInvoke("relayout",
list(xaxis = list(range = c(rl[["xaxis.range[0]"]],
rl[["xaxis.range[1]"]]))))
}
})
plotlyProxy + plotlyProxyInvoke é o equivalente, para gráficos, do leafletProxy/dataTableProxy que veremos nos próximos módulos: modificar um widget existente sem recriá-lo, o que é crítico para performance.
Quando usar cliente, quando usar servidor (guia de decisão)
| Situação | Abordagem | Ferramenta |
|---|---|---|
| Destacar/filtrar pontos entre gráfico e tabela, dataset pequeno | Cliente | crosstalk + highlight/filter_* |
| Relatório sem servidor (Quarto, flexdashboard) | Cliente | crosstalk |
| Clicar numa barra e recalcular uma agregação | Servidor | event_data("plotly_click") |
| Clicar num item e consultar o banco | Servidor | event_data + DBI |
| Ajustar um widget já renderizado sem recriá-lo | Servidor | plotlyProxy/leafletProxy |
| Coordenar 3+ painéis | Servidor | reactiveValues central |
A escolha errada mais comum é usar o servidor para um simples brushing que o crosstalk faria de graça no navegador — introduzindo latência desnecessária. A segunda mais comum é tentar forçar o crosstalk a fazer uma agregação (que ele não faz) em vez de subir para o servidor.
Sincronizar painéis tem duas filosofias, e escolher errado custa caro. No cliente, o crosstalk é instantâneo mas só faz seleção e filtro; no servidor, a reatividade é poderosa e cobra latência. O crosstalk::SharedData liga plotly, leaflet e DT dentro do navegador, com o key unindo as mesmas linhas entre widgets e highlight() e filter_*() controlando o comportamento. Do lado do servidor, o event_data() traz as interações — clique, hover, seleção, relayout — com o key carregando a identidade do elemento e o req() evitando erro antes da primeira interação. A partir de três painéis, centralize tudo num reactiveValues e siga uma regra só: interações escrevem no estado, painéis leem do estado. Para atualizar um gráfico já desenhado sem recriá-lo, plotlyProxy e plotlyProxyInvoke. E o critério de escolha: cliente para brushing leve, servidor quando é preciso recalcular, agregar ou consultar.
Exercícios
-
Você tem uma dispersão e uma tabela num relatório Quarto sem Shiny, e quer que selecionar pontos na dispersão destaque as linhas na tabela. Cliente ou servidor? Como?
✓ Resposta:Cliente, com
crosstalk— não há servidor Shiny num Quarto estático, então a sincronização tem que acontecer no navegador. Envolva os dados numSharedData$new(df, key = ~id)e construa tanto oplot_ly()quanto odatatable()sobre esse mesmo objeto. A seleção por laço no gráfico destaca as linhas correspondentes na tabela automaticamente, sem código de servidor. -
Ao clicar numa barra, seu app precisa rodar uma consulta SQL que agrega milhões de registros. Cliente ou servidor? Por quê?
✓ Resposta:Servidor. O
crosstalksó faz seleção/filtro sobre dados já presentes no navegador — ele não agrega nem consulta bancos, e milhões de registros não caberiam no cliente de qualquer forma. Capture o clique comevent_data("plotly_click", source=), use okeypara saber qual barra foi clicada, e dispare a consulta no servidor (idealmente comDBIe query parametrizada). O resultado alimenta os demais outputs. -
No exemplo com múltiplos
plotly, por que o argumentosource = "..."é necessário?✓ Resposta:Porque
event_data()precisa saber de qual gráfico ler o evento. Semsource, com váriosplotlyna página, os eventos se misturam e você não consegue distinguir um clique no mapa de um clique no ranking. Nomear cada gráfico comsource = "mapa",source = "ranking"etc. e lerevent_data(..., source = "mapa")isola corretamente cada fluxo de interação. -
Explique o padrão "interações escrevem no estado, painéis leem do estado" e por que ele escala melhor que ligar cada painel diretamente aos outros.
✓ Resposta:Em vez de cada painel observar os eventos de todos os demais (o que gera N×N conexões e vira espaguete), centraliza-se um
reactiveValuesque representa o estado do dashboard (região, período, métrica...). Cada interação (clique, filtro) apenas atualiza um campo do estado viaobserveEvent; cada painel apenas lê umreactivederivado do estado (ex.:dados_filtrados()). Assim, adicionar um novo painel é O(1): ele só precisa ler o estado, sem que os outros saibam de sua existência. Há uma única fonte de verdade, o que elimina inconsistências. -
Qual a vantagem de
plotlyProxy(...)sobre simplesmente redesenhar o gráfico dentro de umrenderPlotlya cada mudança?✓ Resposta:plotlyProxy+plotlyProxyInvokemodifica o gráfico já existente no navegador (muda um range de eixo, adiciona um traço, atualiza dados) sem destruí-lo e recriá-lo do zero. Redesenhar viarenderPlotlyreconstrói todo o widget, perde o estado de zoom/pan do usuário, pisca visualmente e é mais lento — especialmente ruim com muitos pontos. O proxy é o mesmo princípio deleafletProxy/dataTableProxy: atualizações incrementais e suaves em vez de recriação completa.