Apostila técnica · edição 2026

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.

Nível 1 · Básico Nível 2 · Intermediário Nível 3 · Avançado Nível 4 · Muito avançado
Repare no design Esta apostila pratica o que ensina: os níveis dos capítulos são codificados com o colormap plasma — uma escala sequencial perceptualmente uniforme, exatamente o tipo de escala que você aprenderá a escolher no Capítulo 1. A barra no topo da página mostra seu progresso de leitura usando o mesmo gradiente.
Parte I · Capítulo 1 Básico

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:

  1. Posição em escala comum — gráfico de dispersão, barras alinhadas.
  2. Posição em escalas não alinhadas — small multiples.
  3. Comprimento — barras.
  4. Ângulo / inclinação — gráficos de linha (inclinação), pizza (ângulo).
  5. Área — bolhas, treemaps.
  6. Volume, curvatura — evite.
  7. Tonalidade e saturação de cor — heatmaps, mapas coropléticos.
Regra de ouro Se a tarefa do leitor é comparar valores com precisão, use posição ou comprimento (barras, pontos). Cor e área servem para dar visão geral de padrões, não para leitura exata. É por isso que a pizza perde da barra em quase todo cenário de comparação.

1.2 Qual gráfico usar? O mapa de decisão

Pergunta que o dado respondeGráfico recomendadoEvite
Comparação entre categoriasBarras (horizontais se rótulos longos)Pizza com >4 fatias
Evolução no tempoLinha; área se for volume acumuladoBarras para séries longas
Relação entre 2 variáveisDispersão (scatter); + cor/tamanho para 3ª e 4ª variávelLinha ligando pontos sem ordem
Distribuição de 1 variávelHistograma, densidade (KDE), boxplot, violinoMédia isolada sem dispersão
Composição (parte do todo)Barras empilhadas 100%, treemap, wafflePizza 3D (sempre)
RankingBarras ordenadas, dot plot, bump chartOrdem alfabética
Dados geográficosCoroplético (taxas), símbolos proporcionais (contagens)Coroplético para valores absolutos
Fluxo entre estadosSankey, chordMuitos 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. Nunca jet/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.
Armadilha clássica ~8% dos homens têm daltonismo vermelho-verde. Nunca use vermelho × verde como único canal de distinção. Teste suas paletas com simuladores (ex.: Viz Palette, Coblis) e reforce a cor com posição, rótulo ou forma. Em entrevista, citar acessibilidade cromática conta ponto.

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.
No mercado Em processos seletivos de dados (analista, cientista, engenheiro de analytics), é comum o case pedir "apresente insights deste dataset". O avaliador olha menos para o gráfico bonito e mais para: escolha correta do tipo de gráfico, escala de cor coerente, eixo honesto e título que afirma o insight ("Churn cai 30% após onboarding" em vez de "Gráfico de churn").
Parte I · Capítulo 2 Básico

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

terminal
# 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ências

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

python · pandas
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

python · pandas
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")
Exercício guiado Baixe o dataset público Gapminder (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.
No mercado Vagas de dados no Brasil pedem quase sempre "pandas avançado" — na prática isso significa: groupby com múltiplas agregações, merge, pivot/melt, datas com resample, e saber explicar por que fez cada transformação.
Parte II · Capítulo 3 Básico → Avançado

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

Modelo mental Figure é a tela inteira; Axes é cada "gráfico" dentro dela (área de plotagem com seus eixos); Axis são os eixos x/y em si. A interface profissional é a orientada a objetos (fig, ax = plt.subplots()), não a plt.plot() global — a global causa bugs em scripts com múltiplas figuras.
python · matplotlib (interface OO)
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

python · layouts
# 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")
Técnica de mercado Small multiples (grade de mini-gráficos com escalas idênticas) é uma das técnicas mais valorizadas em relatórios executivos: substitui um "espaguete" de 12 linhas sobrepostas por 12 painéis limpos e comparáveis. sharex/sharey garante a comparabilidade — sem isso, a técnica perde a validade.

3.3 Identidade visual: rcParams e stylesheets

python · estilo corporativo reutilizável
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

python · anotações e destaque
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

python · controle fino de eixos
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")
Armadilha Eixo Y duplo é fácil de manipular: mudando as escalas você "cria" correlações visuais. Prefira dois painéis empilhados com eixo X compartilhado. Se usar eixo duplo, deixe cada eixo com a cor da sua série.
Parte II · Capítulo 4 Intermediário

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

python · seaborn
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

python · distribuições
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

python · relplot / displot / catplot
# 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)
Axes-level × Figure-level Funções axes-level (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

python · heatmap
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,
)
No mercado Seaborn é a lib nº 1 de EDA em cases de ciência de dados. Um notebook de portfólio forte mostra: 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.
Parte II · Capítulo 5 Intermediário

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

python · plotly express
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")
Técnica de portfólio 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:

python · graph_objects + customização
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

python · animação temporal + coroplético
# 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)

CapacidadeMatplotlib/SeabornPlotly
Hover, zoom, pan nativosNão (estático)Sim, de graça
Exportar PNG/PDF vetorial p/ artigoExcelenteRequer kaleido
Embutir em página webSó como imagemHTML/JS interativo
Controle tipográfico fino p/ impressãoTotalBom, menos fino
Milhões de pontosLento em scatterScattergl (WebGL)
Virar dashboardNão é o propósitoDash 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.

Parte II · Capítulo 6 Intermediário

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.

python · altair
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 operador

Esse 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

python · bokeh server (conceito)
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.py

O 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 realMelhor escolhaPor quê
Figura para artigo/relatório impressoMatplotlibControle tipográfico e vetorial total (PDF/SVG)
EDA rápida em notebookSeabornEstatística embutida, 1 linha por gráfico
Gráfico interativo para stakeholderPlotlyHTML standalone, hover e zoom nativos
Gráficos ligados entre si (brushing)AltairInteratividade declarativa sem servidor
Streaming em tempo realBokehWebSocket + atualização incremental
Dezenas de milhões de pontosDatashader (+Bokeh/Plotly)Rasteriza no servidor (cap. 12)
Dashboard interno rápidoStreamlitVelocidade de desenvolvimento (cap. 7)
Dashboard complexo em produçãoDashCallbacks explícitos, escala melhor (cap. 7)
Visual 100% customizado no site da empresaD3.jsControle absoluto do DOM (cap. 10)
Pergunta de entrevista real "Por que você escolheria Plotly em vez de Matplotlib?" — a resposta fraca é "porque é mais bonito". A resposta forte cita o meio de entrega (navegador × PDF), o tipo de interação necessária e o custo de manutenção. Decorar esta tabela vale mais que decorar APIs.
Parte II · Capítulo 7 Avançado

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

python · app.py (rodar: streamlit run app.py)
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)
Modelo de execução Streamlit re-executa o script inteiro a cada clique. É o que o torna simples — e o que o torna lento sem @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

python · dash
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:server

7.3 O comparativo que decide projetos

CritérioStreamlitDashPanel
Curva de aprendizadoHorasDias (callbacks)Dias
Modelo de execuçãoRe-executa o scriptCallbacks reativosReativo (param/bind)
Controle de layout/CSSLimitadoTotal (é HTML/React)Alto (templates)
Muitos usuários simultâneosSessão pesadaStateless, escala com gunicornMédio
Motor de gráficosQualquer umPlotly (nativo)Bokeh/HoloViews e outros
Melhor paraProtótipos, apps de ML, ferramentas internasProdutos de dados em produçãoEcossistema 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:
Dockerfile mínimo
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"]
No mercado Vaga de Analytics Engineer/Data Analyst sênior frequentemente pede "experiência com Streamlit ou Dash" + Docker. Um repositório com o painel acima, Dockerfile, README com prints e link ao vivo cobre três exigências de uma vez. Saiba justificar a escolha: "usei Streamlit pela velocidade; migraria a Dash se precisasse de autenticação fina e mais usuários simultâneos".
Parte III · Capítulo 8 Básico

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

html
<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

css · grid responsivo com áreas nomeadas
.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

css · tokens + dark mode
: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; }
Detalhe profissional 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.
Parte III · Capítulo 9 Intermediário

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 · um gráfico de barras escrito à mão
<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

javascript · 50 mil pontos em canvas
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érioSVGCanvas 2DWebGL
Limite prático de elementos~1–10 mil~100 mil–1 miMilhões
Interação por elemento (hover, clique)Nativa (DOM)Manual (hit-testing)Manual (picking)
Estilização com CSSSimNãoNão
Acessibilidade (leitores de tela)Boa (ARIA)Requer fallbackRequer fallback
Exportação vetorialNativaSó rasterSó raster
ExemplosD3 clássico, HighchartsChart.js, EChartsdeck.gl, Plotly scattergl
Regra prática Menos de ~5 mil elementos interativos → SVG. Dezenas de milhares → Canvas. Milhões (mapas de pontos, embeddings) → WebGL. Muitos produtos usam híbrido: Canvas para a nuvem de pontos + SVG por cima para eixos e anotações.
Parte III · Capítulo 10 Avançado

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

javascript · d3 v7 — barras a partir de dados
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));
O conceito central: data join 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

javascript · atualização animada
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

javascript · tooltip performático + zoom
// 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

javascript · grafo de força e mapa
// 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çãoUse D3Use Plotly/ECharts/Chart.js
Gráfico padrão (linha, barra) num appCusto alto demaisPronto em minutos
Visual sob medida (identidade única, scrollytelling)É para isso que existeVai limitar você
Data journalism / peça editorialPadrão da indústriaPossível, genérico
Só precisa das escalas/formatosUse módulos avulsos (d3-scale, d3-format) dentro de React
No mercado Poucos profissionais no Brasil dominam D3 de verdade — é um diferencial salarial claro em vagas de front-end de dados, data journalism (grandes redações) e produtos SaaS de analytics. O padrão moderno em produto é React para o DOM + D3 para a matemática (escalas, shapes, forças), deixando o React renderizar o SVG.
Parte III · Capítulo 11 Avançado

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.

python · relatório HTML automatizado
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 sozinho

11.2 Padrão 2 — API + front-end (o padrão de produto)

python · FastAPI servindo dados agregados
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()]
javascript · front-end consumindo a API
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

python + js · streaming
# 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)
javascript · janela deslizante no cliente
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ãoCusto/complexidadeDadosQuando usar
1 · EstáticoMínimoCongelados na geraçãoRelatórios, portfólio
2 · API + frontMédioSob demandaProdutos, apps multiusuário
3 · WebSocketAltoStreaming contínuoMonitoramento, finanças, IoT
4 · BI/embedBaixo (ferramenta)Conexão gerenciadaAnalytics corporativo
Parte IV · Capítulo 12 Muito avançado

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:

python · três formas de reduzir sem mentir
# (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

python · datashader
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 — troque go.Scatter por go.Scattergl e ganhe 10–100× em pontos (WebGL).
  • deck.gl / kepler.gl — milhões de registros geoespaciais na GPU; pydeck expõ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.
python · duckdb alimentando o gráfico
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 instantaneamente
No mercado "Como você plotaria 100 milhões de linhas?" é pergunta clássica de entrevista sênior. A resposta esperada é uma escada de estratégias: agregar na origem (SQL/DuckDB) → binning/datashader → WebGL só se a granularidade individual for realmente necessária → e questionar se o usuário precisa mesmo daquele volume na tela.
Parte IV · Capítulo 13 Muito avançado

Storytelling 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)

  1. Contexto: quem decide? O que essa pessoa precisa fazer depois de ver o gráfico?
  2. Um insight por visual: se o gráfico responde três perguntas, são três gráficos.
  3. Título-afirmação: "Churn caiu 30% após o novo onboarding" — nunca "Análise de churn".
  4. Atenção dirigida: contexto em cinza, o dado-chave em UMA cor de destaque (técnica do cap. 3.4).
  5. 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

Teste dos 5 segundos Um bom dashboard responde "estamos bem ou mal?" em 5 segundos. Estruture em camadas: topo = KPIs com comparativo (▲/▼ vs período anterior); meio = tendências que explicam os KPIs; base = detalhe/tabela para investigação. O olhar em telas segue o padrão F/Z: o canto superior esquerdo é o imóvel mais caro da tela — coloque lá o número mais importante.
  • 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:

html + css + js · esqueleto de scrollytelling
<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-label descritivo no SVG e tabela de dados alternativa para leitores de tela.
  • prefers-reduced-motion respeitado: animações viram estados finais.
  • Interações também por teclado (foco visível nos elementos clicáveis).
Parte IV · Capítulo 14 Carreira

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

TrilhaStack central desta apostilaComplementos cobrados
Analista de Dados / BICaps. 1–5, 13 (fundamentos, Seaborn, Plotly, storytelling)SQL forte, Power BI ou Looker, Excel
Cientista de DadosCaps. 2–5 (EDA), 7 (apps de modelo)Estatística, ML (scikit-learn), experimentação
Analytics EngineerCaps. 7, 11, 12 (dashboards, APIs, DuckDB)dbt, SQL avançado, orquestração, Git/CI
Front-end de dados / DataVis EngineerCaps. 8–11 (CSS, SVG, D3, integração)React/TypeScript, testes, design systems
Data JournalismCaps. 1, 10, 13 (percepção, D3, scrollytelling)Apuração, redação, mapas (QGIS)

14.2 Portfólio: 3 projetos > 30 certificados

  1. 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.
  2. 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.
  3. 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.
Regras do portfólio que converte Cada projeto com: link ao vivo (recrutador não clona repositório), README com problema → decisões → resultado, código limpo com nomes em um idioma só, e um post curto no LinkedIn explicando o insight (distribuição importa tanto quanto obra).

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

FaseCapítulosEntrega concreta
Semanas 1–31, 2, 310 gráficos Matplotlib refeitos "do jeito certo" (tema próprio, anotações)
Semanas 4–64, 5, 6Projeto 1 do portfólio (análise narrada) publicado
Semanas 7–97, 11Projeto 2: dashboard com deploy + Docker
Semanas 10–148, 9, 103 gráficos D3 do zero (barras animadas, scatter com zoom, mapa)
Semanas 15–1812, 13, 14Projeto 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! ▮▮▮▮▮