Visualização de dados,
do básico ao muito avançado
Um guia prático de Python e web design para quem quer trabalhar com dados: cada capítulo tem código real, comparativos honestos entre tecnologias e orientação direta sobre o que o mercado cobra.
Fundamentos de visualização de dados
Antes de qualquer linha de código: visualização é codificar dados em atributos visuais (posição, comprimento, cor, área, ângulo). Quem domina esses fundamentos escolhe o gráfico certo em segundos — e isso é o que separa quem "faz gráfico" de quem comunica com dados.
1.1 A hierarquia da percepção (Cleveland & McGill)
Pesquisas clássicas de percepção mostraram que nós, humanos, decodificamos alguns atributos visuais com muito mais precisão do que outros. Da leitura mais precisa para a menos precisa:
- Posição em escala comum — gráfico de dispersão, barras alinhadas.
- Posição em escalas não alinhadas — small multiples.
- Comprimento — barras.
- Ângulo / inclinação — gráficos de linha (inclinação), pizza (ângulo).
- Área — bolhas, treemaps.
- Volume, curvatura — evite.
- Tonalidade e saturação de cor — heatmaps, mapas coropléticos.
1.2 Qual gráfico usar? O mapa de decisão
| Pergunta que o dado responde | Gráfico recomendado | Evite |
|---|---|---|
| Comparação entre categorias | Barras (horizontais se rótulos longos) | Pizza com >4 fatias |
| Evolução no tempo | Linha; área se for volume acumulado | Barras para séries longas |
| Relação entre 2 variáveis | Dispersão (scatter); + cor/tamanho para 3ª e 4ª variável | Linha ligando pontos sem ordem |
| Distribuição de 1 variável | Histograma, densidade (KDE), boxplot, violino | Média isolada sem dispersão |
| Composição (parte do todo) | Barras empilhadas 100%, treemap, waffle | Pizza 3D (sempre) |
| Ranking | Barras ordenadas, dot plot, bump chart | Ordem alfabética |
| Dados geográficos | Coroplético (taxas), símbolos proporcionais (contagens) | Coroplético para valores absolutos |
| Fluxo entre estados | Sankey, chord | Muitos nós sem agregação |
1.3 Cor: a decisão mais errada do iniciante
Existem três tipos de escala de cor, e trocar um pelo outro é o erro mais comum em dashboards profissionais:
- Categórica (qualitativa) — categorias sem ordem (produtos, regiões). Máximo de ~7 cores distinguíveis. Ex.:
tab10,Set2. - Sequencial — de pouco a muito (vendas, temperatura). Use colormaps perceptualmente uniformes:
viridis,plasma,cividis. Nuncajet/arco-íris — ele cria fronteiras falsas nos dados. - Divergente — valores em torno de um centro significativo (lucro/prejuízo, anomalia de temperatura). Ex.:
RdBu,coolwarm— o centro da escala deve coincidir com o zero real do dado.
1.4 Integridade: proporcionalidade e o eixo Y
- Barras começam no zero. Sempre — comprimento truncado mente.
- Linhas podem ter eixo truncado quando o objetivo é mostrar variação (ex.: câmbio), mas deixe o corte explícito.
- Área e bolha escalam pela área, não pelo raio. Dobrar o raio quadruplica a percepção.
- Data-ink ratio (Tufte): remova tudo que não carrega informação — bordas, grades pesadas, sombras, 3D decorativo.
Ambiente e pandas essencial para DataVis
Todo gráfico começa com dados no formato certo. 80% dos problemas de visualização em Python são, na verdade, problemas de formato de dados — e o conceito central é tidy data.
2.1 Setup profissional
# Ambiente isolado por projeto (padrão de mercado)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pandas numpy matplotlib seaborn plotly streamlit altair
pip freeze > requirements.txt # sempre versione as dependênciasPara exploração use Jupyter ou VS Code com notebooks; para produção, scripts .py versionados no Git. Empresas esperam os dois fluxos.
2.2 Tidy data: o formato que todas as libs esperam
Formato tidy (longo): cada variável é uma coluna, cada observação é uma linha. Seaborn, Plotly Express e Altair são construídos em torno disso.
import pandas as pd
# Formato "largo" (comum em planilhas de empresa)
largo = pd.DataFrame({
"produto": ["A", "B"],
"jan": [100, 80],
"fev": [120, 95],
"mar": [140, 90],
})
# melt() converte para o formato longo (tidy)
tidy = largo.melt(id_vars="produto", var_name="mes", value_name="vendas")
print(tidy)
# produto mes vendas
# 0 A jan 100
# 1 B jan 80
# 2 A fev 120 ...
# O caminho inverso: pivot() — útil para heatmaps
matriz = tidy.pivot(index="produto", columns="mes", values="vendas")2.3 As 6 operações que resolvem 90% da preparação
df = pd.read_csv("vendas.csv", parse_dates=["data"])
# 1. Filtrar
df_2025 = df[df["data"].dt.year == 2025]
# 2. Agregar (a base de quase todo gráfico de negócio)
por_regiao = (df.groupby("regiao", as_index=False)
.agg(receita=("valor", "sum"),
ticket_medio=("valor", "mean"),
pedidos=("valor", "size")))
# 3. Ordenar (ranking correto ANTES de plotar)
por_regiao = por_regiao.sort_values("receita", ascending=False)
# 4. Reamostrar séries temporais
mensal = df.set_index("data")["valor"].resample("ME").sum()
# 5. Janela móvel (suavização honesta de tendência)
mensal_suave = mensal.rolling(3, center=True).mean()
# 6. Tratar nulos de forma explícita
df["categoria"] = df["categoria"].fillna("Não informado")plotly.data.gapminder()) e produza:
(1) receita... digo, expectativa de vida média por continente por década com
groupby; (2) o mesmo dataframe em formato largo com pivot.
Você vai reutilizar os dois nos capítulos 3–6.
groupby com múltiplas agregações, merge, pivot/melt,
datas com resample, e saber explicar por que fez cada transformação.
Matplotlib a fundo: do primeiro plot ao controle total
Matplotlib é a fundação de todo o ecossistema (Seaborn e pandas plotam sobre ele). Dominá-lo significa nunca ficar travado: qualquer detalhe visual é customizável.
3.1 A anatomia: Figure, Axes, Axis
fig, ax = plt.subplots()), não a plt.plot() global — a global
causa bugs em scripts com múltiplas figuras.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 12, 200)
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=110)
ax.plot(x, np.sin(x), label="Receita", linewidth=2)
ax.plot(x, np.sin(x) * 0.6 + 0.3, label="Custo", linewidth=2, linestyle="--")
ax.set_title("Interface orientada a objetos", loc="left", fontweight="bold")
ax.set_xlabel("Mês")
ax.set_ylabel("R$ milhões")
ax.legend(frameon=False)
ax.spines[["top", "right"]].set_visible(False) # data-ink ratio!
fig.tight_layout()
fig.savefig("grafico.png", dpi=200, bbox_inches="tight")3.2 Subplots e layouts complexos
# Grade simples com eixos compartilhados (small multiples)
fig, axs = plt.subplots(2, 3, figsize=(11, 6), sharex=True, sharey=True)
for ax, regiao in zip(axs.flat, ["N", "NE", "CO", "SE", "S", "BR"]):
ax.set_title(regiao, loc="left", fontsize=10, fontweight="bold")
# Layout irregular declarativo (recurso moderno e elegante)
fig, axd = plt.subplot_mosaic(
"""
AAB
AAC
""",
figsize=(10, 5), layout="constrained",
)
axd["A"].set_title("Gráfico principal")
axd["B"].set_title("KPI 1")
axd["C"].set_title("KPI 2")sharex/sharey garante a
comparabilidade — sem isso, a técnica perde a validade.
3.3 Identidade visual: rcParams e stylesheets
import matplotlib as mpl
# Tema global do projeto (coloque num módulo theme.py e importe em todo notebook)
mpl.rcParams.update({
"figure.dpi": 110,
"font.family": "sans-serif",
"font.sans-serif": ["Inter", "Arial"],
"axes.spines.top": False,
"axes.spines.right": False,
"axes.grid": True,
"grid.alpha": 0.25,
"axes.prop_cycle": mpl.cycler(color=[
"#0D0887", "#8006A8", "#CB4679", "#D66B00", "#3B7A57",
]),
})
# Alternativa: salve como arquivo .mplstyle e use plt.style.use("empresa.mplstyle")3.4 Anotações: onde nasce o storytelling Avançado
fig, ax = plt.subplots(figsize=(9, 4.5))
serie = mensal_suave # do capítulo 2
ax.plot(serie.index, serie.values, color="#B9BDC7", lw=1.6) # contexto em cinza
ax.plot(serie.index[-6:], serie.values[-6:], color="#0D0887", lw=3) # foco em cor
pico = serie.idxmax()
ax.annotate(
"Pico após campanha\nde Black Friday",
xy=(pico, serie.max()), # ponto anotado
xytext=(30, -40), textcoords="offset points",
arrowprops=dict(arrowstyle="->", color="#444"),
fontsize=10,
)
ax.axvspan(serie.index[-6], serie.index[-1], alpha=0.08, color="#0D0887")
ax.set_title("Receita mensal: destaque no último semestre",
loc="left", fontweight="bold")Padrão profissional: cinza para contexto, uma única cor forte para o dado que importa, e a anotação dentro do gráfico contando o "porquê". Esse trio aparece em The Economist, Nubank, iFood e em todo portfólio que impressiona.
3.5 Eixos avançados: escalas, datas e eixos duplos
import matplotlib.dates as mdates
import matplotlib.ticker as mticker
fig, ax = plt.subplots()
ax.set_yscale("log") # crescimento exponencial vira reta
ax.yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f"R$ {v:,.0f}".replace(",", "."))
)
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b/%y"))
# Eixo duplo — use com parcimônia e cores casadas com as séries
ax2 = ax.twinx()
ax2.set_ylabel("Conversão (%)", color="#CB4679")
ax2.tick_params(axis="y", labelcolor="#CB4679")Seaborn: visualização estatística em poucas linhas
Seaborn traduz dataframes tidy em gráficos estatísticos com uma linha — e como devolve objetos Matplotlib, você refina com tudo do capítulo anterior. É a dupla perfeita para análise exploratória (EDA).
4.1 A regra semântica: x, y, hue, size, style
import seaborn as sns
sns.set_theme(style="whitegrid", context="notebook")
tips = sns.load_dataset("tips")
# Cada coluna vira um canal visual — mesma gramática em toda a lib
ax = sns.scatterplot(
data=tips, x="total_bill", y="tip",
hue="time", # cor = categoria
size="size", # área = nº de pessoas
style="smoker", # forma = fumante
palette="plasma",
)
ax.set_title("Gorjetas: 5 variáveis num único gráfico", loc="left")4.2 Distribuições: o kit completo
fig, axs = plt.subplots(1, 3, figsize=(13, 3.6))
sns.histplot(data=tips, x="total_bill", kde=True, ax=axs[0])
sns.boxplot(data=tips, x="day", y="total_bill", ax=axs[1])
sns.violinplot(data=tips, x="day", y="total_bill",
inner="quart", ax=axs[2])
axs[0].set_title("Histograma + KDE")
axs[1].set_title("Boxplot (mediana/quartis)")
axs[2].set_title("Violino (forma completa)")Quando usar cada um: histograma para uma variável isolada; boxplot para comparar muitas categorias de forma compacta; violino quando a forma da distribuição importa (bimodalidade que o boxplot esconde). Para públicos leigos, prefira histograma — boxplot exige leitor treinado.
4.3 Figure-level: small multiples automáticos
# col/row criam a grade automaticamente a partir dos DADOS
g = sns.relplot(
data=tips, x="total_bill", y="tip",
col="time", row="smoker", # 2×2 painéis
hue="day", kind="scatter", height=3,
)
g.set_titles("{row_name} · {col_name}")
g.figure.suptitle("Faceting: a força do formato tidy", y=1.03)scatterplot, boxplot) desenham num
ax e se integram a layouts Matplotlib. Funções figure-level
(relplot, catplot, displot) criam a figura inteira e
ganham faceting grátis — mas não entram num subplot existente. Saber essa diferença evita
horas de frustração.
4.4 Correlação e heatmap anotado
corr = tips.select_dtypes("number").corr()
sns.heatmap(
corr, annot=True, fmt=".2f",
cmap="RdBu_r", center=0, # divergente centrada no zero!
square=True, linewidths=.5,
)pairplot/heatmap para triagem, distribuições por segmento,
e um gráfico final refinado com Matplotlib puro. Isso demonstra a progressão
exploração → comunicação, exatamente o que entrevistadores procuram.
Plotly: interatividade para a web
Plotly gera gráficos interativos em HTML/JavaScript a partir de Python: hover, zoom, pan e seleção de graça. É a ponte natural entre o notebook e o navegador — e a base do framework Dash.
5.1 Plotly Express: o caminho rápido
import plotly.express as px
gap = px.data.gapminder().query("year == 2007")
fig = px.scatter(
gap, x="gdpPercap", y="lifeExp",
size="pop", color="continent",
hover_name="country",
log_x=True, size_max=55,
labels={"gdpPercap": "PIB per capita (log)",
"lifeExp": "Expectativa de vida"},
title="Gapminder 2007 — cada bolha é um país",
)
fig.show() # abre interativo
fig.write_html("gapminder.html", # arquivo standalone p/ portfólio
include_plotlyjs="cdn")write_html() gera um arquivo que roda em qualquer navegador sem servidor.
Hospede no GitHub Pages e você tem visualizações interativas com link público —
diferencial enorme num currículo, custo zero.
5.2 Graph Objects: controle total Avançado
Express cobre 80% dos casos; para os 20% restantes (múltiplos tipos de traço, subplots mistos, formas customizadas) use graph_objects:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
fig = make_subplots(rows=1, cols=2, subplot_titles=("Receita", "Margem %"),
column_widths=[0.65, 0.35])
fig.add_trace(go.Bar(x=meses, y=receita, name="Receita",
marker_color="#0D0887"), row=1, col=1)
fig.add_trace(go.Scatter(x=meses, y=margem, mode="lines+markers",
name="Margem", line=dict(color="#CB4679", width=3)),
row=1, col=2)
fig.update_layout(
template="plotly_white",
font_family="Inter",
hovermode="x unified", # tooltip único por posição x
legend=dict(orientation="h", y=1.12),
margin=dict(t=70, r=20),
)
# Tooltip customizado — detalhe que separa amador de profissional
fig.update_traces(
hovertemplate="<b>%{x}</b><br>R$ %{y:,.0f}<extra></extra>",
row=1, col=1,
)5.3 Animações e mapas
# Animação: uma linha extra transforma o scatter em "filme" por ano
fig = px.scatter(px.data.gapminder(),
x="gdpPercap", y="lifeExp", size="pop", color="continent",
animation_frame="year", animation_group="country",
log_x=True, size_max=55, range_y=[25, 90])
# Mapa coroplético mundial em 6 linhas
fig = px.choropleth(
px.data.gapminder().query("year == 2007"),
locations="iso_alpha", color="lifeExp",
color_continuous_scale="Viridis",
title="Expectativa de vida (2007)",
)5.4 O que o Plotly faz que Matplotlib não faz (e vice-versa)
| Capacidade | Matplotlib/Seaborn | Plotly |
|---|---|---|
| Hover, zoom, pan nativos | Não (estático) | Sim, de graça |
| Exportar PNG/PDF vetorial p/ artigo | Excelente | Requer kaleido |
| Embutir em página web | Só como imagem | HTML/JS interativo |
| Controle tipográfico fino p/ impressão | Total | Bom, menos fino |
| Milhões de pontos | Lento em scatter | Scattergl (WebGL) |
| Virar dashboard | Não é o propósito | Dash nativo |
Regra prática de mercado: relatório em PDF/artigo científico → Matplotlib; análise exploratória → Seaborn; qualquer coisa que vive no navegador → Plotly.
Altair, Bokeh e o comparativo definitivo
Além do trio principal, duas bibliotecas importam para decisões de arquitetura: Altair (gramática de gráficos declarativa) e Bokeh (interatividade server-side). Saber quando usar cada tecnologia é uma habilidade de senioridade.
6.1 Altair: pensando em gramática de gráficos
Altair implementa a Grammar of Graphics (a mesma teoria do ggplot2 do R e do Vega-Lite): você declara dados → marcas → codificações e a biblioteca decide o desenho.
import altair as alt
from vega_datasets import data
cars = data.cars()
# Seleção interativa DECLARATIVA: brush liga dois gráficos sem callback
brush = alt.selection_interval()
pontos = alt.Chart(cars).mark_circle(size=60).encode(
x="Horsepower:Q",
y="Miles_per_Gallon:Q",
color=alt.condition(brush, "Origin:N", alt.value("lightgray")),
).add_params(brush)
barras = alt.Chart(cars).mark_bar().encode(
x="count():Q", y="Origin:N", color="Origin:N",
).transform_filter(brush)
(pontos & barras) # & empilha; | põe lado a lado — composição por operadorEsse exemplo — brushing & linking entre dois gráficos — exigiria dezenas de linhas com callbacks em outras libs. É a grande força do paradigma declarativo. A limitação histórica era o volume de dados embutido no JSON; o modo vegafusion mitiga isso agregando no servidor.
6.2 Bokeh: widgets e streaming
from bokeh.plotting import figure, curdoc
from bokeh.models import Slider, ColumnDataSource
source = ColumnDataSource(dict(x=x, y=np.sin(x)))
p = figure(height=300, title="Bokeh server: Python reage no browser")
p.line("x", "y", source=source, line_width=3)
def atualizar(attr, old, novo):
source.data = dict(x=x, y=np.sin(novo * x)) # muda dados, não redesenha
freq = Slider(start=0.5, end=5, value=1, step=0.1, title="Frequência")
freq.on_change("value", atualizar)
curdoc().add_root(p) # rodar com: bokeh serve app.pyO diferencial do Bokeh é o ColumnDataSource sincronizado entre Python e navegador via WebSocket — ideal para dados em streaming (IoT, mercado financeiro). Ele também é o motor de renderização do ecossistema HoloViews/Panel.
6.3 Tabela-mestre: qual biblioteca Python usar quando
| Cenário real | Melhor escolha | Por quê |
|---|---|---|
| Figura para artigo/relatório impresso | Matplotlib | Controle tipográfico e vetorial total (PDF/SVG) |
| EDA rápida em notebook | Seaborn | Estatística embutida, 1 linha por gráfico |
| Gráfico interativo para stakeholder | Plotly | HTML standalone, hover e zoom nativos |
| Gráficos ligados entre si (brushing) | Altair | Interatividade declarativa sem servidor |
| Streaming em tempo real | Bokeh | WebSocket + atualização incremental |
| Dezenas de milhões de pontos | Datashader (+Bokeh/Plotly) | Rasteriza no servidor (cap. 12) |
| Dashboard interno rápido | Streamlit | Velocidade de desenvolvimento (cap. 7) |
| Dashboard complexo em produção | Dash | Callbacks explícitos, escala melhor (cap. 7) |
| Visual 100% customizado no site da empresa | D3.js | Controle absoluto do DOM (cap. 10) |
Dashboards em Python: Streamlit × Dash (× Panel)
Dashboard é o produto de dados mais pedido pelo mercado. Em Python, a decisão-chave é Streamlit (velocidade) versus Dash (controle e escala). Vamos construir o mesmo painel nos dois para sentir a diferença.
7.1 Streamlit: o dashboard em 30 linhas
import streamlit as st
import plotly.express as px
import pandas as pd
st.set_page_config(page_title="Painel de Vendas", layout="wide")
@st.cache_data # evita reler o CSV a cada interação — ESSENCIAL
def carregar():
return pd.read_csv("vendas.csv", parse_dates=["data"])
df = carregar()
# --- Sidebar de filtros ---
st.sidebar.header("Filtros")
regioes = st.sidebar.multiselect("Região", df["regiao"].unique(),
default=list(df["regiao"].unique()))
periodo = st.sidebar.date_input("Período",
(df["data"].min(), df["data"].max()))
f = df[df["regiao"].isin(regioes)]
f = f[f["data"].between(*map(pd.Timestamp, periodo))]
# --- KPIs ---
c1, c2, c3 = st.columns(3)
c1.metric("Receita", f"R$ {f['valor'].sum():,.0f}",
delta=f"{f['valor'].sum()/df['valor'].sum():.0%} do total")
c2.metric("Pedidos", f"{len(f):,}")
c3.metric("Ticket médio", f"R$ {f['valor'].mean():,.2f}")
# --- Gráficos ---
esq, dir = st.columns([2, 1])
mensal = f.set_index("data")["valor"].resample("ME").sum().reset_index()
esq.plotly_chart(px.line(mensal, x="data", y="valor",
title="Receita mensal"), use_container_width=True)
top = f.groupby("regiao")["valor"].sum().sort_values().reset_index()
dir.plotly_chart(px.bar(top, x="valor", y="regiao", orientation="h",
title="Por região"), use_container_width=True)@st.cache_data (dados) e
@st.cache_resource (conexões/modelos). Estado entre execuções vive em
st.session_state. Entender isso é o que diferencia quem "usa" de quem domina.
7.2 Dash: callbacks explícitos para produção
from dash import Dash, dcc, html, Input, Output
import plotly.express as px
app = Dash(__name__)
app.layout = html.Div([
html.H2("Painel de Vendas"),
dcc.Dropdown(id="regiao",
options=sorted(df["regiao"].unique()),
value="Sudeste"),
dcc.Graph(id="grafico"),
])
# Só ESTA função roda quando o dropdown muda — nada de re-executar tudo
@app.callback(Output("grafico", "figure"), Input("regiao", "value"))
def atualizar(regiao):
f = df[df["regiao"] == regiao]
mensal = f.set_index("data")["valor"].resample("ME").sum().reset_index()
return px.line(mensal, x="data", y="valor",
title=f"Receita mensal — {regiao}")
if __name__ == "__main__":
app.run(debug=True) # produção: gunicorn app:server7.3 O comparativo que decide projetos
| Critério | Streamlit | Dash | Panel |
|---|---|---|---|
| Curva de aprendizado | Horas | Dias (callbacks) | Dias |
| Modelo de execução | Re-executa o script | Callbacks reativos | Reativo (param/bind) |
| Controle de layout/CSS | Limitado | Total (é HTML/React) | Alto (templates) |
| Muitos usuários simultâneos | Sessão pesada | Stateless, escala com gunicorn | Médio |
| Motor de gráficos | Qualquer um | Plotly (nativo) | Bokeh/HoloViews e outros |
| Melhor para | Protótipos, apps de ML, ferramentas internas | Produtos de dados em produção | Ecossistema científico/HoloViz |
7.4 Deploy: colocar no ar (e no currículo)
- Streamlit Community Cloud — deploy grátis direto do GitHub; perfeito para portfólio.
- Docker + provedor (Render, Railway, Cloud Run, EC2) — o caminho corporativo:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0"]Web design para dados: HTML, CSS e layout moderno
Todo dashboard vive numa página. Dominar HTML semântico, CSS Grid e design tokens permite construir a "casa" das suas visualizações — e customizar Streamlit, Dash e relatórios HTML muito além do padrão.
8.1 Estrutura semântica de um dashboard
<body>
<header class="topo">
<h1>Painel de Vendas</h1>
<nav aria-label="Períodos">…</nav>
</header>
<main class="grade">
<section class="kpi" aria-label="Receita total">
<span class="kpi-rotulo">Receita</span>
<span class="kpi-valor">R$ 4,2 mi</span>
<span class="kpi-delta positivo">▲ 12% vs mês anterior</span>
</section>
<figure class="card grafico-principal">
<figcaption>Receita mensal por região</figcaption>
<div id="chart-receita"></div> <!-- Plotly/D3 monta aqui -->
</figure>
</main>
</body>Semântica (main, section, figure, aria-label) não é burocracia: melhora acessibilidade, SEO e a manutenção do código — três palavras que aparecem em toda vaga de front-end de dados.
8.2 CSS Grid: o layout de dashboard em 20 linhas
.grade{
display:grid;
gap:1rem;
grid-template-columns:repeat(4, 1fr);
grid-template-areas:
"kpi1 kpi2 kpi3 kpi4"
"main main main lateral"
"mapa mapa tabela tabela";
}
.grafico-principal{grid-area:main}
.painel-lateral{grid-area:lateral}
/* KPIs que se adaptam sozinhos, sem media query */
.linha-kpis{
display:grid;
grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));
gap:1rem;
}
@media (max-width:800px){
.grade{
grid-template-columns:1fr;
grid-template-areas:"kpi1" "kpi2" "kpi3" "kpi4"
"main" "lateral" "mapa" "tabela";
}
}8.3 Design tokens: o tema como variáveis
:root{
--bg:#FBFBF8; --ink:#151920; --card:#FFFFFF;
--serie-1:#0D0887; --serie-2:#CB4679; --positivo:#1B7A3D; --negativo:#B3261E;
--raio:12px; --sombra:0 4px 16px rgb(0 0 0 / .07);
--fonte-num:"JetBrains Mono", monospace; /* números tabulares em KPIs */
}
@media (prefers-color-scheme: dark){
:root{ --bg:#12151C; --ink:#E8ECF3; --card:#1B2029; }
}
.card{ background:var(--card); border-radius:var(--raio); box-shadow:var(--sombra); }
.kpi-valor{ font-family:var(--fonte-num); font-variant-numeric:tabular-nums; }font-variant-numeric: tabular-nums faz todos os dígitos ocuparem a mesma
largura — números alinham em tabelas e KPIs não "pulam" quando atualizam. É o tipo de
detalhe que um recrutador técnico de front-end de dados nota na hora.
8.4 Tipografia e hierarquia para leitura de dados
- Escala tipográfica: defina 4–5 tamanhos (ex.: 12/14/18/24/36 px) e não invente outros. Valor do KPI grande, rótulo pequeno em caixa alta com
letter-spacing. - Contraste mínimo WCAG: 4.5:1 para texto normal. Cinza claro sobre branco em rótulos de eixo reprova em auditoria de acessibilidade.
- Alinhamento: números à direita, texto à esquerda; nunca centralize colunas numéricas.
- Densidade: dashboards executivos pedem respiro (muito espaço em branco); dashboards operacionais (NOC, trading) pedem densidade. Pergunte quem usa antes de desenhar.
SVG e Canvas: como o navegador desenha dados
Toda visualização web termina em SVG (elementos vetoriais no DOM) ou Canvas (pixels via JavaScript). Entender os dois é pré-requisito para D3 e para decisões de performance.
9.1 SVG: gráficos como elementos HTML
<svg viewBox="0 0 320 160" role="img" aria-label="Vendas por trimestre">
<!-- eixo y=140 é a linha de base -->
<line x1="30" y1="140" x2="310" y2="140" stroke="#999"/>
<!-- ATENÇÃO: o y do SVG cresce para BAIXO.
Barra de altura h começa em y = 140 - h -->
<rect x="45" y="60" width="50" height="80" fill="#0D0887"/>
<rect x="115" y="40" width="50" height="100" fill="#0D0887"/>
<rect x="185" y="90" width="50" height="50" fill="#0D0887"/>
<rect x="255" y="20" width="50" height="120" fill="#CB4679"/>
<text x="280" y="14" font-size="10" text-anchor="middle">recorde</text>
</svg>Por serem nós do DOM, elementos SVG aceitam CSS, eventos de mouse e ARIA — é por isso que o D3 é construído sobre SVG. O viewBox define um sistema de coordenadas interno que escala responsivamente.
9.2 Canvas: pixels para volume
const cv = document.querySelector("canvas");
const ctx = cv.getContext("2d");
// Nítido em telas retina: desenhe em resolução física
const dpr = window.devicePixelRatio || 1;
cv.width = 800 * dpr; cv.height = 500 * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = "rgba(13, 8, 135, 0.25)"; // alpha revela densidade
for (let i = 0; i < 50_000; i++) {
const x = Math.random() * 800;
const y = 250 + (Math.random() - 0.5) * (x * 0.6);
ctx.fillRect(x, y, 2, 2); // fillRect >> arc em volume
}9.3 SVG × Canvas × WebGL: a decisão
| Critério | SVG | Canvas 2D | WebGL |
|---|---|---|---|
| Limite prático de elementos | ~1–10 mil | ~100 mil–1 mi | Milhões |
| Interação por elemento (hover, clique) | Nativa (DOM) | Manual (hit-testing) | Manual (picking) |
| Estilização com CSS | Sim | Não | Não |
| Acessibilidade (leitores de tela) | Boa (ARIA) | Requer fallback | Requer fallback |
| Exportação vetorial | Nativa | Só raster | Só raster |
| Exemplos | D3 clássico, Highcharts | Chart.js, ECharts | deck.gl, Plotly scattergl |
D3.js: do zero ao avançado
D3 (Data-Driven Documents) não é uma biblioteca de gráficos: é uma biblioteca para ligar dados ao DOM. Você não escolhe um "tipo de gráfico" — você constrói qualquer visual imaginável. É a tecnologia por trás dos gráficos do NYT, Observable e de todo visual premiado.
10.1 Os 3 pilares: seleções, escalas, eixos
import * as d3 from "d3";
const dados = [
{ regiao: "SE", receita: 420 }, { regiao: "S", receita: 260 },
{ regiao: "NE", receita: 200 }, { regiao: "CO", receita: 120 },
];
const m = { top: 20, right: 16, bottom: 28, left: 44 };
const W = 560, H = 300;
// ESCALAS: funções que mapeiam domínio (dados) → alcance (pixels)
const x = d3.scaleBand()
.domain(dados.map(d => d.regiao))
.range([m.left, W - m.right]).padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(dados, d => d.receita)]).nice()
.range([H - m.bottom, m.top]); // invertido: y do SVG desce!
const svg = d3.select("#chart").append("svg")
.attr("viewBox", [0, 0, W, H]);
// DATA JOIN: um <rect> para cada item do array
svg.selectAll("rect")
.data(dados)
.join("rect")
.attr("x", d => x(d.regiao))
.attr("y", d => y(d.receita))
.attr("width", x.bandwidth())
.attr("height", d => y(0) - y(d.receita))
.attr("fill", "#0D0887");
// EIXOS: geradores que desenham ticks a partir das escalas
svg.append("g").attr("transform", `translate(0,${H - m.bottom})`)
.call(d3.axisBottom(x));
svg.append("g").attr("transform", `translate(${m.left},0)`)
.call(d3.axisLeft(y).ticks(5));selection.data(dados).join(...) declara uma correspondência
dado ↔ elemento. Quando os dados mudam, o D3 calcula quem
entra (enter), quem muda (update) e quem
sai (exit). Todo o resto do D3 é consequência dessa ideia.
10.2 Update pattern + transições animadas
function render(dados) {
y.domain([0, d3.max(dados, d => d.receita)]).nice();
svg.selectAll("rect")
.data(dados, d => d.regiao) // chave estável = animação correta
.join(
enter => enter.append("rect")
.attr("x", d => x(d.regiao))
.attr("width", x.bandwidth())
.attr("y", y(0)).attr("height", 0) // nasce "do chão"
.attr("fill", "#0D0887"),
update => update,
exit => exit.transition().duration(400)
.attr("y", y(0)).attr("height", 0).remove()
)
.transition().duration(600).ease(d3.easeCubicOut)
.attr("y", d => y(d.receita))
.attr("height", d => y(0) - y(d.receita));
}10.3 Interações avançadas: tooltip, brush e zoom Muito avançado
// Tooltip: um único div reutilizado (nunca crie um por ponto)
const tip = d3.select("body").append("div").attr("class", "tooltip");
svg.selectAll("rect")
.on("pointermove", (evento, d) => {
tip.style("opacity", 1)
.html(`<b>${d.regiao}</b><br>R$ ${d.receita} mi`)
.style("left", (evento.pageX + 12) + "px")
.style("top", (evento.pageY - 24) + "px");
})
.on("pointerleave", () => tip.style("opacity", 0));
// Zoom semântico: reescala os eixos, não estica os pixels
svg.call(d3.zoom()
.scaleExtent([1, 8])
.on("zoom", ({ transform }) => {
const zx = transform.rescaleX(x0); // x0 = escala original
eixoX.call(d3.axisBottom(zx));
pontos.attr("cx", d => zx(d.data));
}));
// d3.brush() cria seleção retangular → filtre outros gráficos no evento "end"
// (é assim que se implementa crossfiltering "na mão")10.4 Layouts prontos: força, hierarquia, geo
// Grafo de rede: a simulação calcula posições, você só desenha
const sim = d3.forceSimulation(nos)
.force("link", d3.forceLink(arestas).id(d => d.id).distance(60))
.force("charge", d3.forceManyBody().strength(-180))
.force("center", d3.forceCenter(W / 2, H / 2))
.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
no.attr("cx", d => d.x).attr("cy", d => d.y);
});
// Mapa: projeção transforma lon/lat → pixels; path desenha GeoJSON
const proj = d3.geoMercator().fitSize([W, H], geojsonBrasil);
const path = d3.geoPath(proj);
svg.selectAll("path").data(geojsonBrasil.features).join("path")
.attr("d", path)
.attr("fill", d => escalaCor(taxaPorUF.get(d.properties.sigla)));10.5 D3 × bibliotecas de alto nível: quando compensa?
| Situação | Use D3 | Use Plotly/ECharts/Chart.js |
|---|---|---|
| Gráfico padrão (linha, barra) num app | Custo alto demais | Pronto em minutos |
| Visual sob medida (identidade única, scrollytelling) | É para isso que existe | Vai limitar você |
| Data journalism / peça editorial | Padrão da indústria | Possível, genérico |
| Só precisa das escalas/formatos | Use módulos avulsos (d3-scale, d3-format) dentro de React | — |
Integração Python ↔ Web: arquiteturas reais
Na empresa, a visualização raramente vive sozinha: ela consome dados de uma API, atualiza em tempo real ou é embutida num produto. Aqui estão os quatro padrões de arquitetura que você vai encontrar.
11.1 Padrão 1 — Arquivo estático (o mais simples)
Python gera HTML/JSON → hospedagem estática (GitHub Pages, S3). Zero servidor, zero custo. Ideal para relatórios periódicos e portfólio. Limite: dados "congelados" no momento da geração.
figs = [fig_kpi, fig_serie, fig_mapa]
partes = [f.to_html(full_html=False, include_plotlyjs="cdn" if i == 0
else False) for i, f in enumerate(figs)]
html = f"""<html><head><meta charset='utf-8'>
<link rel='stylesheet' href='tema.css'></head>
<body><h1>Relatório semanal</h1>{''.join(partes)}</body></html>"""
open("relatorio.html", "w").write(html)
# Agende com cron/GitHub Actions → relatório se atualiza sozinho11.2 Padrão 2 — API + front-end (o padrão de produto)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import pandas as pd
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"]) # restrinja em prod
df = pd.read_parquet("vendas.parquet")
@app.get("/api/receita-mensal")
def receita_mensal(regiao: str | None = None):
f = df if regiao is None else df[df["regiao"] == regiao]
serie = (f.set_index("data")["valor"].resample("ME").sum())
# REGRA DE OURO: agregue no servidor, envie KB — nunca o dataset cru
return [{"mes": k.strftime("%Y-%m"), "valor": round(v, 2)}
for k, v in serie.items()]async function desenhar(regiao = null) {
const url = "/api/receita-mensal" + (regiao ? `?regiao=${regiao}` : "");
const dados = await (await fetch(url)).json();
Plotly.react("chart", [{
x: dados.map(d => d.mes),
y: dados.map(d => d.valor),
type: "scatter", mode: "lines", line: { color: "#0D0887", width: 3 },
}], { margin: { t: 24 }, yaxis: { tickprefix: "R$ " } });
// Plotly.react atualiza sem recriar o gráfico — importante p/ UX
}Esse é o padrão de separação de responsabilidades: Python cuida de dados e regra de negócio; o front (Plotly.js, ECharts, D3, React) cuida da experiência. É a arquitetura de quase todo SaaS de analytics.
11.3 Padrão 3 — Tempo real com WebSocket Muito avançado
# servidor (FastAPI)
from fastapi import WebSocket
import asyncio, json, random
@app.websocket("/ws/metricas")
async def metricas(ws: WebSocket):
await ws.accept()
while True:
await ws.send_text(json.dumps({"v": random.gauss(100, 8)}))
await asyncio.sleep(1)const ws = new WebSocket("ws://localhost:8000/ws/metricas");
const buffer = [];
ws.onmessage = (ev) => {
buffer.push(JSON.parse(ev.data).v);
if (buffer.length > 60) buffer.shift(); // últimos 60s
Plotly.react("live", [{ y: [...buffer], type: "scatter" }],
{ yaxis: { range: [60, 140] } }); // eixo FIXO: sem "pulos"
};11.4 Padrão 4 — Embed e ferramentas de BI
- iframe: embutir um app Streamlit/Dash ou um HTML do Plotly em qualquer página/Notion/intranet.
- BI corporativo (Power BI, Looker, Tableau, Metabase): em muitas empresas você não vai programar o dashboard — vai preparar os dados e aplicar os princípios dos capítulos 1 e 13 na ferramenta. Os fundamentos transferem 1:1.
| Padrão | Custo/complexidade | Dados | Quando usar |
|---|---|---|---|
| 1 · Estático | Mínimo | Congelados na geração | Relatórios, portfólio |
| 2 · API + front | Médio | Sob demanda | Produtos, apps multiusuário |
| 3 · WebSocket | Alto | Streaming contínuo | Monitoramento, finanças, IoT |
| 4 · BI/embed | Baixo (ferramenta) | Conexão gerenciada | Analytics corporativo |
Performance e grandes volumes de dados
Com 500 pontos qualquer técnica funciona. Com 50 milhões, a visualização vira um problema de engenharia. Este capítulo cobre as estratégias que resolvem — em ordem de custo.
12.1 A estratégia nº 1: agregue antes de plotar
Ninguém enxerga 10 milhões de pontos individuais — o olho percebe densidade e tendência. Então entregue exatamente isso:
# (a) Binning 2D: scatter gigante → mapa de densidade honesto
fig, ax = plt.subplots()
hb = ax.hexbin(df["x"], df["y"], gridsize=60, cmap="viridis",
mincnt=1, bins="log")
fig.colorbar(hb, label="pontos por célula (log)")
# (b) Downsample de séries temporais: 10 mi de ticks → 1 ponto/minuto
ohlc = df.set_index("ts")["preco"].resample("1min").ohlc()
# (c) Percentis em vez de pontos: banda de incerteza
q = (df.groupby("dia")["latencia"]
.quantile([.05, .5, .95]).unstack())
ax.fill_between(q.index, q[0.05], q[0.95], alpha=.2, label="p5–p95")
ax.plot(q.index, q[0.5], lw=2, label="mediana")12.2 Datashader: rasterização server-side
import datashader as ds, datashader.transfer_functions as tf
import colorcet
canvas = ds.Canvas(plot_width=900, plot_height=500)
agg = canvas.points(df, "lon", "lat") # agrega N milhões → grade
img = tf.shade(agg, cmap=colorcet.fire, how="eq_hist")
# how="eq_hist": equalização de histograma revela estrutura
# em dados com cauda longa — o "truque" que faz os mapas famosos
tf.set_background(img, "black").to_pil().save("densidade.png")O princípio: cada pixel vira um agregado estatístico — nunca há overplotting, e a imagem enviada ao navegador pesa o mesmo com mil ou com um bilhão de pontos. Com HoloViews + Bokeh, o zoom dispara re-rasterização dinâmica.
12.3 No navegador: WebGL e o resto do arsenal
- Plotly
scattergl— troquego.Scatterporgo.Scattergle ganhe 10–100× em pontos (WebGL). - deck.gl / kepler.gl — milhões de registros geoespaciais na GPU;
pydeckexpõe isso ao Python. - Formato de transporte: troque JSON por Apache Arrow/Parquet em pipelines pesados; JSON de 100 MB derruba a aba do usuário.
- Backend colunar: pré-agregue com DuckDB/Polars — agregações de bilhões de linhas em segundos, direto de arquivos Parquet.
import duckdb
agg = duckdb.sql("""
SELECT date_trunc('day', ts) AS dia,
quantile_cont(latencia, 0.5) AS p50,
quantile_cont(latencia, 0.95) AS p95
FROM 'logs/*.parquet' -- lê bilhões de linhas sem carregar tudo
GROUP BY 1 ORDER BY 1
""").df()
# agg tem ~365 linhas: qualquer biblioteca plota instantaneamenteStorytelling com dados e design de dashboards
Tecnicamente perfeito e ignorado pelo público é o destino de muito gráfico. Este capítulo é sobre a camada que gera promoções: transformar análise em decisão.
13.1 O framework de narrativa (Storytelling with Data)
- Contexto: quem decide? O que essa pessoa precisa fazer depois de ver o gráfico?
- Um insight por visual: se o gráfico responde três perguntas, são três gráficos.
- Título-afirmação: "Churn caiu 30% após o novo onboarding" — nunca "Análise de churn".
- Atenção dirigida: contexto em cinza, o dado-chave em UMA cor de destaque (técnica do cap. 3.4).
- Chamada para ação: o slide/painel termina com a recomendação, não com o gráfico.
13.2 Hierarquia de dashboard: a pirâmide de 5 segundos
- KPI sem contexto é decoração: sempre acompanhe de delta, meta ou sparkline.
- Filtros globais no topo/lateral, aplicados a tudo; nunca filtros escondidos por gráfico.
- Máximo ~7 visuais por tela; mais que isso, crie abas por pergunta de negócio ("Aquisição", "Retenção", "Receita").
- Estados vazios e de erro desenhados: "Sem dados para este filtro — tente ampliar o período" em vez de um gráfico em branco.
13.3 Scrollytelling: a técnica editorial Muito avançado
O formato das grandes reportagens visuais (NYT, The Pudding, Nexo): o texto rola e o gráfico, fixo, se transforma a cada passo. A implementação moderna usa IntersectionObserver:
<section class="scrolly">
<div class="grafico-fixo"><svg id="viz"></svg></div>
<article>
<div class="passo" data-passo="0"><p>Em 2015, as regiões eram parecidas…</p></div>
<div class="passo" data-passo="1"><p>…até o Sudeste descolar em 2020…</p></div>
<div class="passo" data-passo="2"><p>…e hoje concentrar 60% da receita.</p></div>
</article>
</section>
<style>
.scrolly{ display:grid; grid-template-columns:1fr 1fr; }
.grafico-fixo{ position:sticky; top:0; height:100vh; } /* o truque todo */
.passo{ min-height:90vh; display:flex; align-items:center; }
</style>
<script>
const obs = new IntersectionObserver((entradas) => {
entradas.forEach(e => {
if (e.isIntersecting) atualizarViz(+e.target.dataset.passo);
// atualizarViz() usa o update pattern do cap. 10.2:
// mesmo SVG, dados/destaques diferentes por passo
});
}, { threshold: 0.6 });
document.querySelectorAll(".passo").forEach(p => obs.observe(p));
</script>13.4 Acessibilidade como requisito, não enfeite
- Contraste de texto ≥ 4.5:1; elementos gráficos essenciais ≥ 3:1 (WCAG).
- Paleta segura para daltonismo + redundância (rótulo direto na série > legenda distante).
aria-labeldescritivo no SVG e tabela de dados alternativa para leitores de tela.prefers-reduced-motionrespeitado: animações viram estados finais.- Interações também por teclado (foco visível nos elementos clicáveis).
Mercado de trabalho: transformando skill em contratação
DataVis não é um cargo — é uma competência multiplicadora presente em várias trilhas. Este capítulo mapeia as trilhas, monta seu portfólio e prepara a entrevista.
14.1 As trilhas e o que cada uma cobra
| Trilha | Stack central desta apostila | Complementos cobrados |
|---|---|---|
| Analista de Dados / BI | Caps. 1–5, 13 (fundamentos, Seaborn, Plotly, storytelling) | SQL forte, Power BI ou Looker, Excel |
| Cientista de Dados | Caps. 2–5 (EDA), 7 (apps de modelo) | Estatística, ML (scikit-learn), experimentação |
| Analytics Engineer | Caps. 7, 11, 12 (dashboards, APIs, DuckDB) | dbt, SQL avançado, orquestração, Git/CI |
| Front-end de dados / DataVis Engineer | Caps. 8–11 (CSS, SVG, D3, integração) | React/TypeScript, testes, design systems |
| Data Journalism | Caps. 1, 10, 13 (percepção, D3, scrollytelling) | Apuração, redação, mapas (QGIS) |
14.2 Portfólio: 3 projetos > 30 certificados
- Análise narrada (caps. 1–5): notebook + página HTML com dados públicos brasileiros (IBGE, dados.gov.br, Base dos Dados). EDA em Seaborn → 3 gráficos finais polidos → conclusões escritas. Mostra pensamento analítico.
- Dashboard no ar (cap. 7 + 11): Streamlit ou Dash com filtros, KPIs e deploy público + Dockerfile + README com prints e decisões justificadas. Mostra engenharia.
- Peça visual autoral (caps. 8–10 + 13): uma visualização D3 sob medida ou scrollytelling sobre um tema que você ama. É o projeto que gera conversa em entrevista — ninguém mais terá um igual.
14.3 Perguntas de entrevista — e a estrutura da boa resposta
- "Critique este dashboard." → Estruture: público e tarefa → escolha de gráficos (cap. 1.2) → cor e escala (1.3) → hierarquia (13.2) → uma melhoria priorizada. Critique o quê, proponha o como.
- "Pizza ou barra?" → Hierarquia perceptual (1.1): comparação precisa pede posição/comprimento; pizza só para "parte do todo" com ≤ 3 fatias e sem comparação fina.
- "Streamlit ou Dash?" → Tabela 7.3: prazo, nº de usuários, necessidade de customização, quem mantém.
- "100 milhões de pontos?" → Escada do cap. 12: agregar na origem → binning/datashader → WebGL → questionar o requisito.
- Case ao vivo: verbalize as decisões enquanto codifica ("vou ordenar as barras antes de plotar porque ranking sem ordem esconde o insight"). Avaliam o raciocínio, não a memória de API.
14.4 Roadmap de estudo sugerido
| Fase | Capítulos | Entrega concreta |
|---|---|---|
| Semanas 1–3 | 1, 2, 3 | 10 gráficos Matplotlib refeitos "do jeito certo" (tema próprio, anotações) |
| Semanas 4–6 | 4, 5, 6 | Projeto 1 do portfólio (análise narrada) publicado |
| Semanas 7–9 | 7, 11 | Projeto 2: dashboard com deploy + Docker |
| Semanas 10–14 | 8, 9, 10 | 3 gráficos D3 do zero (barras animadas, scatter com zoom, mapa) |
| Semanas 15–18 | 12, 13, 14 | Projeto 3: peça autoral/scrollytelling + revisão dos anteriores com lente de storytelling |
14.5 Checklist final de empregabilidade
- Sei justificar a escolha de gráfico e de escala de cor em qualquer visual meu.
- Tenho 3 projetos públicos com link ao vivo e README profissional.
- Domino o par exploração (Seaborn) → comunicação (Matplotlib/Plotly refinado).
- Construí e fiz deploy de pelo menos um dashboard (Streamlit ou Dash).
- Sei ler e escrever SVG à mão e explicar SVG × Canvas × WebGL.
- Implementei o data join + update pattern do D3 sem copiar código.
- Sei desenhar a arquitetura API + front e explicar quando usar cada padrão do cap. 11.
- Consigo responder "como plotar 100 mi de linhas" com a escada de estratégias.
- Meus dashboards passam no teste dos 5 segundos e no checklist de acessibilidade.
- Publico o que aprendo (LinkedIn/blog) — visibilidade também é skill.
Fim da apostila — mas não do estudo. Refaça os códigos com seus dados: é na adaptação que o conhecimento vira competência. Boa jornada! ▮▮▮▮▮