6  Visualização de dados

Julia tem três caminhos razoáveis para gráficos, todos assentados sobre Makie.jl (Danisch e Krumbiegel 2021):

Caminho Para quem
TidierPlots.jl quem vem do ggplot2 e quer a mesma gramática
AlgebraOfGraphics.jl quem quer gramática de gráficos idiomática em Julia
Makie.jl puro controle total sobre a figura

Este capítulo mostra os três sobre o mesmo conjunto de dados, porque a escolha certa depende da tarefa — e porque saber traduzir entre eles evita ficar preso.

using TidierData
using CairoMakie
using AlgebraOfGraphics
using CSV, DataFrames

CairoMakie.activate!(type = "png")

ponds = CSV.read(joinpath("..", "data", "ponds.csv"), DataFrame)
ponds_c = @chain ponds begin
    @drop_missing(canopy)
    @mutate(log_area = log(area_m2))
end

nrow(ponds_c)
173
NotaExercícios deste capítulo

Uma figura não pode ser verificada automaticamente; por isso, na maioria dos quadros Sua vez, você compara o seu gráfico com o da solução. Comece cada sessão carregando os pacotes e os dados, com a pasta do livro aberta no VS Code:

include("exercises/checar.jl")
using TidierData, TidierPlots, CairoMakie, AlgebraOfGraphics, CSV, DataFrames, Statistics
ponds = CSV.read(joinpath("data", "ponds.csv"), DataFrame)
ponds_c = @chain ponds begin
    @drop_missing(canopy)
    @mutate(log_area = log(area_m2))
end

6.1 TidierPlots.jl: a gramática do ggplot

using TidierPlots

ggplot(ponds_c, @aes(x = canopy, y = richness, color = region)) +
    geom_point(alpha = 0.7) +
    labs(x = "Cobertura de dossel (%)", y = "Riqueza de espécies",
         title = "Riqueza declina com o sombreamento")
Figura 6.1: Riqueza em função da cobertura de dossel, por região.

Em R

ggplot(ponds_c, aes(x = canopy, y = richness, colour = region)) +
  geom_point(alpha = 0.7) +
  labs(x = "Canopy cover (%)", y = "Species richness")

A tradução é quase caractere a caractere; a diferença é o @ em @aes.

NotaSua vez 6.1 — Riqueza e temperatura

Refaça a Figura 6.1 com a temperatura da água (temp_c) no eixo x e os pontos coloridos pelo hidroperíodo (hydro). Dê nome aos dois eixos. A riqueza muda com a temperatura tão claramente quanto com o dossel?

Este não tem verificador: compare a sua figura com a da solução.

Só mudam o mapeamento em @aes e os rótulos.

ggplot(ponds_c, @aes(x = temp_c, y = richness, color = hydro)) +
    geom_point(alpha = 0.7) +
    labs(x = "Temperatura da água (°C)", y = "Riqueza de espécies")

A nuvem é bem mais achatada que a do dossel. A simulação inclui um efeito da temperatura (scripts/make_data.jl), mas pequeno: cerca de 3% a mais de espécies por grau, invisível diante da variação entre lagoas.

6.2 AlgebraOfGraphics.jl: gramática algébrica

AoG trata gráficos como uma álgebra: * combina camadas, + as sobrepõe.

plt = AlgebraOfGraphics.data(ponds_c) *
      mapping(:canopy => "Cobertura de dossel (%)",
              :richness => "Riqueza de espécies",
              color = :region => "Região") *
      (visual(Scatter, alpha = 0.6) + AlgebraOfGraphics.linear())

draw(plt; figure = (size = (700, 450),))
Figura 6.2: O mesmo gráfico em AlgebraOfGraphics, com ajuste linear por região.
Nota

AlgebraOfGraphics.data e AlgebraOfGraphics.linear são qualificados porque os nomes data e linear colidem com outros pacotes carregados. Em um script que use só AoG, data(...) e linear() bastam.

NotaSua vez 6.2 — Tamanho corporal, com retas ajustadas

Com AlgebraOfGraphics, faça o gráfico de body_size contra temp_c, colorido por hydro, com um ajuste linear para cada hidroperíodo. Lagoas permanentes e temporárias diferem?

Este não tem verificador: compare a sua figura com a da solução.

Parta do bloco acima e mude o mapping. As retas ajustadas seguem automaticamente o agrupamento por cor.

plt = AlgebraOfGraphics.data(ponds_c) *
      mapping(:temp_c => "Temperatura da água (°C)",
              :body_size => "Tamanho corporal (mm)",
              color = :hydro => "Hidroperíodo") *
      (visual(Scatter, alpha = 0.6) + AlgebraOfGraphics.linear())

draw(plt; figure = (size = (700, 450),))

As duas retas quase coincidem: na simulação, o tamanho corporal depende da temperatura e do dossel, mas não do hidroperíodo. Testamos isso formalmente no Capítulo 8.

6.3 Facetas

plt = AlgebraOfGraphics.data(ponds_c) *
      mapping(:log_area => "log(área)",
              :richness => "Riqueza",
              color = :region,
              layout = :hydro) *
      (visual(Scatter, alpha = 0.6) + AlgebraOfGraphics.linear())

draw(plt; figure = (size = (800, 400),))
Figura 6.3: Relação riqueza–área facetada por hidroperíodo.
NotaSua vez 6.3 — Facetas na gramática do ggplot

Traduza a Figura 6.3 para TidierPlots.jl, usando facet_wrap para separar os painéis por hidroperíodo. Deixe de fora as retas ajustadas (veja o aviso na solução).

Este não tem verificador: compare a sua figura com a da solução.

facet_wrap(facets = :hydro) é adicionado com +, como qualquer outra camada.

ggplot(ponds_c, @aes(x = log_area, y = richness, color = region)) +
    geom_point(alpha = 0.6) +
    facet_wrap(facets = :hydro) +
    labs(x = "log(área)", y = "Riqueza")
AvisoUm limite do TidierPlots

No TidierPlots.jl 1.0.0 (conferido em setembro de 2026), adicionar geom_smooth(method = "lm") a um gráfico com facetas falha com column name :facet not found. geom_smooth funciona sem facetas, e facetas funcionam sem geom_smooth, mas não os dois juntos. Quando a gramática do ggplot chega a um limite como este, o AlgebraOfGraphics (Figura 6.3) é a saída.

6.4 Distribuições

plt = AlgebraOfGraphics.data(ponds_c) *
      mapping(:region => "Região",
              :richness => "Riqueza",
              color = :hydro => "Hidroperíodo",
              dodge = :hydro) *
      visual(BoxPlot)

draw(plt; figure = (size = (700, 420),))
Figura 6.4: Distribuição da riqueza por região e hidroperíodo.
NotaSua vez 6.4 — A forma de uma distribuição

Boxplots escondem a forma da distribuição. Mostre a distribuição de body_size em cada região como violinos (Violin), com uma cor por região.

Este não tem verificador: compare a sua figura com a da solução.

Troque BoxPlot por Violin e mapeie region tanto no eixo x quanto em color.

plt = AlgebraOfGraphics.data(ponds_c) *
      mapping(:region => "Região",
              :body_size => "Tamanho corporal (mm)",
              color = :region) *
      visual(Violin)

draw(plt; figure = (size = (700, 420),))

6.5 Makie.jl puro: controle total

Quando a figura precisa de um layout que a gramática não expressa, desça ao Makie.

fig = Figure(size = (860, 380))

ax1 = Axis(fig[1, 1], xlabel = "Cobertura de dossel (%)", ylabel = "Riqueza",
           title = "(a) Dossel")
scatter!(ax1, ponds_c.canopy, ponds_c.richness, color = (:steelblue, 0.6))

ax2 = Axis(fig[1, 2], xlabel = "log(área)", ylabel = "Riqueza",
           title = "(b) Área")
scatter!(ax2, ponds_c.log_area, ponds_c.richness, color = (:darkorange, 0.6))

ax3 = Axis(fig[1, 3], xlabel = "Riqueza", ylabel = "Frequência",
           title = "(c) Distribuição")
hist!(ax3, ponds_c.richness, bins = 12, color = (:seagreen, 0.7))

fig
Figura 6.5: Figura composta montada manualmente com Makie.

O sistema de layout do Makie é baseado em grade (fig[linha, coluna]), cobrindo o papel de patchwork e cowplot no R sem pacote adicional.

NotaSua vez 6.5 — Dois painéis à mão

Monte uma figura com size = (800, 350) e dois painéis: à esquerda, um boxplot da riqueza por região; à direita, um gráfico de barras com a proporção de lagoas com a espécie-foco em cada região (os números do 5.4). Guarde-a em fig_regioes.

Este não tem verificador: compare a sua figura com a da solução.

boxplot! e barplot! do Makie querem números no eixo x. Dê um número a cada região com [findfirst(==(r), regs) for r in ponds.region], onde regs = ["Amazon", "Atlantic", "Cerrado"], e recoloque os nomes com xticks = (1:3, regs) em cada Axis.

regs = ["Amazon", "Atlantic", "Cerrado"]
idx = [findfirst(==(r), regs) for r in ponds.region]
props = [mean(ponds.focal[ponds.region .== r]) for r in regs]

fig_regioes = Figure(size = (800, 350))

ax1 = Axis(fig_regioes[1, 1], xticks = (1:3, regs),
           xlabel = "Região", ylabel = "Riqueza", title = "(a) Riqueza")
boxplot!(ax1, idx, ponds.richness, color = (:steelblue, 0.6))

ax2 = Axis(fig_regioes[1, 2], xticks = (1:3, regs),
           xlabel = "Região", ylabel = "Proporção de lagoas", title = "(b) Espécie-foco")
barplot!(ax2, 1:3, props, color = (:darkorange, 0.8))

fig_regioes

Os dois painéis contam histórias diferentes: a riqueza varia muito entre regiões; a ocorrência da espécie-foco, quase nada.

6.6 Salvando figuras

save("figura.png", fig; px_per_unit = 3)   # raster em alta resolução
save("figura.pdf", fig)                    # vetorial, para publicação
save("figura.svg", fig)
NotaSua vez 6.6 — Uma figura para publicação

Salve a fig_regioes do 6.5 como focal-by-region.png, com três pixels por unidade, na pasta do livro. Depois verifique o arquivo: checar("6.6", "focal-by-region.png").

O bloco acima mostra o argumento que define a resolução.

save("focal-by-region.png", fig_regioes; px_per_unit = 3)
checar("6.6", "focal-by-region.png")

Uma figura de 800 × 350 com três pixels por unidade gera uma imagem de 2400 × 1050 px — suficiente para uma figura de duas colunas em um periódico. Para saída vetorial, salve em .pdf.

DicaEscolhendo o backend

CairoMakie gera saída estática de qualidade de publicação (PNG, PDF, SVG) e é o que este livro usa. GLMakie abre janelas interativas com aceleração de GPU — útil para explorar dados, inadequado para gerar um livro.

Desafios

  1. No 6.1 você julgou o efeito da temperatura a olho. Adicione um ajuste linear por hidroperíodo com geom_smooth(method = "lm"). A reta muda a sua leitura do gráfico? O que você precisaria saber antes de confiar nela?
  2. O painel (b) do 6.5 mostra proporções sem incerteza. Adicione a cada barra uma barra de erro de \(\pm 2\) erros-padrão, com \(\text{EP} = \sqrt{p(1-p)/n}\) e errorbars!. As regiões ainda parecem diferentes?
  3. Refaça a Figura 6.5 com uma cor por região em todos os painéis e uma única legenda compartilhada à direita (Legend(fig[1, 4], ...)).