Gráficos ASCII
Gráficos em estilo terminal construídos a partir de caracteres Unicode de bloco simples e incorporados como texto literal:
um bloco de código cercado em Markdown, uma tag <pre> em HTML. Sem serviço de gráfico, sem rodapé "feito com",
sem conta externa, e eles renderizam de forma idêntica onde quer que monoespaço renderize.
scripts/ascii_chart.py tem uma função por tipo de gráfico. Execute python3 scripts/ascii_chart.py
para imprimir um exemplo de cada tipo como verificação de sanidade.
Duas coisas aqui foram medidas em vez de assumidas, e ambas estavam erradas inicialmente: em uma viewport de telefone de 390px, linhas com mais de ~44 caracteres cortam a borda direita, e um círculo "redondo" em monoespaço precisa de uma correção de aspecto horizontal de cerca de 2.49 (não 2.0). Consulte Dimensionamento e a seção Pizza/rosca.
As plataformas de newsletter diferem em como colam blocos de código. Antes de confiar em uma, cole um gráfico de teste em um rascunho real e verifique as renderizações na web, aplicativo e e-mail.
Conjunto de caracteres
█▓▒░(U+2588, U+2593, U+2592, U+2591 — sombreamento cheio/escuro/médio/claro) — densidade de preenchimento, de "mais" a "menos." Barras usam apenas█/░(preenchido vs. trilha); pizza/rosca usam todos os quatro (mais▚/▞/▤/▥se houver mais de 4 fatias) como preenchimentos distintos por fatia; o mapa de calor usa todos os quatro mais·(ponto médio, não um espaço em branco) para o nível mais baixo, para que uma célula "silenciosa" ainda seja lida como parte intencional da grade, não uma lacuna.- dígitos
1–7— códigos de região de Venn (uma máscara de bits de qual(is) conjunto(s) uma região pertence — veja Venn abaixo) +— marcador de extremidade aberta em uma barra de faixa que pode ser maior que o teto mostrado (não estique a barra em si para fingir um número que você não tem)┃— tick de destino do gráfico de marcador, desenhado sobre qualquer preenchimento/sombreamento que esteja por baixo, para que esteja sempre visível└─┘— colchete de lacuna, abrangendo entre duas extremidades de barra★/☆— estrela preenchida/vazia, funcionando como rótulo de linha do histograma de estrelas- desenho de caixa (
─│╭╮╰╯) — conectores de gráfico de linha e linha de tendência ●— marcador de ponto de dispersão
Sem cor, em lugar nenhum. Esse é o meio, não uma limitação a ser contornada — um bloco de código colado do Substack são caracteres literais, nada mais sobrevive (sem CSS inline, sem spans estilizados). Qualquer distinção que um gráfico precise fazer reside no caractere/dígito/legenda, não em um canal de cor indisponível.
Menu de tipos de gráfico
| Tarefa | Função | Use para |
|---|---|---|
| Comparação de magnitude, várias categorias, todas começam em 0 | bar(valor, valor_máx, largura) + renderizar() | Abastecimento de gasolina (5 min) vs. carga rápida DC (45 min) |
| Uma faixa min–max, uma categoria | barra_faixa(início, fim, escala_máx, largura, extremidade_aberta=) + renderizar() | "$100k–$300k+ instalado" |
| Dois valores, antes/depois | Texto simples: f"{início} → {fim}" | "Atacado $220 → bar $650" — uma barra não adiciona nada; a seta é o gráfico |
| Uma medida vs. uma faixa de referência + um alvo | marcador(valor, escala_máx, alvo=, referência=) + renderizar() | "Sua classificação: 4.2, faixa normal 3.8–4.5, alvo 4.7" |
| Destacar explicitamente a lacuna entre duas barras | colchete_lacuna(a, b, escala_máx, largura, rótulo=) via renderizar(colchete=) | "Você vs. mediana do mercado" — o movimento característico do relatório |
| Uma distribuição de classificação de 1 a 5 (ou similar) | histograma_estrelas(contagens) | Detalhamento de estrelas do Google/Yelp, uma linha por contagem de estrelas |
| Dois eixos categóricos, sombreados por intensidade | mapa_calor(matriz, rótulos_linha, marcas_coluna=) | Tráfego de pedestres por dia × hora — "quando você está ocupado/morto" |
| Tendência ao longo do tempo, uma série | gráfico_linha(valores, altura, rótulos_x=) | Receita mensal, contagem de avaliações ao longo de um ano |
| Tendência, inline em uma frase | minigráfico(valores) | "Avaliações por mês: ▂▃▅▆█▇▅▃, aumento de 3x desde março" |
| 2 ou 3 conjuntos nomeados, onde o grau de sobreposição é a própria descoberta | venn(rótulos) | "38% de suas avaliações de uma estrela também mencionam tempo de espera" — o tamanho da sobreposição é o ponto |
| Partes de um todo, um instantâneo | pizza(fatias, raio) | Mix de receita: café / comida quente / bebidas / loteria |
| Partes de um todo, com um total do título | rosca(fatias, raio, texto_centro=) | O mesmo, com "247 avaliações" sentado no centro oco |
| Duas variáveis numéricas, existe uma relação | dispersão(pontos, largura, altura) | Preço vs. classificação entre concorrentes |
| O mesmo, mais "e aqui está a tendência" | dispersão_com_tendência(pontos, largura, altura) | O mesmo, com uma linha ajustada provando a correlação |
Se nada nesta tabela se encaixar, pare e pense bem em vez de forçar uma forma que não se encaixa — um gráfico tecnicamente renderizável, mas confuso, é pior do que recorrer ao pipeline PNG headless do Chrome.
O teste de 2 segundos para gráficos
Um gráfico responde ao mesmo padrão que uma boa frase faz: ser compreendido por um leitor cansado em seu telefone, na primeira olhada, sem reler, sem decodificar. Aplique o teste antes de escolher um tipo de gráfico, não depois de construir um.
O teste de 2 segundos: o leitor pretendido — um proprietário de pequena empresa, não um analista de dados — poderia dizer o que este gráfico significa em até 2 segundos, sem ler uma legenda primeiro? Se entender até mesmo uma região ou fatia requer decodificar uma legenda primeiro, isso é o equivalente visual do jargão: tecnicamente preciso, mas faz o leitor trabalhar em vez de atraí-lo.
Pego em uma questão real, pelo editor lendo o post renderizado a frio:
- O diagrama de máscara de bits de dígitos de Venn de 3 conjuntos exigia a leitura de uma legenda de 7 linhas antes que qualquer região significasse algo. O editor: eles não entenderam, então um empresário comum também não entenderia. O fato subjacente — "você precisa de todos esses três, não apenas um" — é uma frase, e a lista numerada logo após o diagrama já dizia isso em inglês simples. O diagrama adicionou custo de decodificação sem adicionar informação que a prosa já não carregava mais claramente. Correção: remova -o, deixe a frase + lista fazerem o trabalho.
- A barra de faixa de custo do carregador DC rápido (uma única barra, sem eixo, sem ponto de comparação) não tinha nada para comparar. O valor de uma barra vem de mostrar uma magnitude relativa a algo — uma escala, outra barra, uma referência. Uma barra solitária sem ponto de referência é decorativa, não informativa. O número que ilustrava ($100k–$300k+) já estava declarado claramente uma frase antes. Correção: remova-a também.
Regras que se seguem:
- Um gráfico que precisa de sua própria legenda para ser lido é um sinal de alerta — não apenas para Venn. Um mapa de calor com eixos não rotulados, ou uma pizza com mais fatias do que um leitor pode manter na cabeça, falha da mesma forma.
- Se uma frase simples afirma o fato tão claramente quanto o gráfico faria, pule o gráfico. Mesmo instinto de trocar "vários/muitos/vários" por a contagem real na prosa: recorra a um visual porque uma comparação ou tendência é o ponto, não apenas porque os dados têm uma forma.
- Cada barra precisa de um eixo (
renderizar(..., eixo=)) ou de uma segunda barra na mesma escala para comparar. Uma única barra sem nenhum dos dois não transmite nada — a mesma falha de uma frase na voz passiva sem sujeito real. Mas uma vez que isso for satisfeito, remova o eixo se ele apenas reiterar um número já presente em um rótulo de valor por linha — uma barra de gasolina vs. carregamento removeu sua linha de eixo0 ... 45 minexatamente por este motivo: ambas as linhas já carregam seu próprio valor ("5 min", "45 min"), então o eixo apenas repetia "45 min" uma segunda vez. Mesmo princípio de cortar uma cláusula redundante de uma frase. - Não crie gráfico para um número que já é uma frase limpa. "$100k–$300k+ instalado" é maximamente claro como texto; criar um gráfico para isso não revela uma comparação, tendência ou sobreposição, apenas adiciona uma etapa de decodificação sem retorno.
- Leia o gráfico finalizado a frio, da forma que o leitor real o lerá — um proprietário de posto de gasolina em seu telefone, não a pessoa que acabou de construí-lo e já sabe o que ele deve dizer.
Fluxo de trabalho
Use scripts/ascii_chart.py em vez de alinhar caracteres manualmente ou
rasterizar geometria você mesmo — matemática de colunas e matemática de círculo/ângulo são
exatamente o tipo de coisa que é fácil de errar sutilmente a olho e só
aparece como um desalinhamento de um caractere ou um círculo desequilibrado depois do
fato. Execute python3 scripts/ascii_chart.py a qualquer momento para imprimir um
exemplo de cada tipo de gráfico como verificação de sanidade.
Barra / barra de faixa
from ascii_chart import bar, barra_faixa, renderizar
linhas = [
("Abastecimento de gasolina", bar(5, 45, 24), "5 min"),
("Carga rápida DC", bar(45, 45, 24), "45 min"),
]
print(renderizar(linhas, eixo=("0", "45 min")))
linha = ("Carregador DC rápido", barra_faixa(100, 300, 300, 24, extremidade_aberta=True), "$100k–$300k+")
print(renderizar([linha]))
renderizar() recebe tuplas (rótulo, string_barra, string_valor) e lida com todo o
alinhamento de colunas — passe seu próprio string_valor pré-formatado (moeda,
unidades, o que a cópia precisar); a função apenas lida com geometria.
Gráfico de marcador
from ascii_chart import marcador, renderizar
linhas = [("Classificação média", marcador(4.2, 5.0, alvo=4.7, referência=(3.8, 4.5), largura=24), "4.2 (alvo 4.7)")]
print(renderizar(linhas, eixo=("0", "5.0")))
Uma barra sólida para o valor medido, desenhada sobre uma faixa de referência
sombreada opcional (a faixa "normal", ▒) com um tick de alvo opcional
(┃, sempre desenhado por último para que permaneça visível sobre qualquer preenchimento/sombreamento abaixo
dele). Responde "estou dentro do normal e quão longe do alvo" em um
relance — ainda há apenas uma barra real (a medida); a faixa é contexto.
Colchete de lacuna
from ascii_chart import bar, colchete_lacuna, renderizar
linhas = [("Você", bar(42000, 60000, 24), "$42,000"), ("Mediana do mercado", bar(58000, 60000, 24), "$58,000")]
print(renderizar(linhas, colchete=colchete_lacuna(42000, 58000, 60000, 24)))
Um colchete └─┘ abrangendo entre duas extremidades de barra, com o tamanho da lacuna
centralizado dentro dele (auto-formatado a partir dos dois valores, ou passe rótulo=
para seu próprio texto). Use o mesmo escala_máx/largura para as barras e
o colchete, ou o intervalo não se alinhará. Isso é "você vs. o mercado",
tornado explícito em vez de deixado para o leitor estimar.
Histograma de estrelas
from ascii_chart import histograma_estrelas
print(histograma_estrelas([(5, 61), (4, 22), (3, 9), (2, 4), (1, 4)]))
contagens é [(estrelas, contagem), ...] em qualquer ordem que você deseja imprimir
(geralmente 5 até 1). Os glifos de estrela preenchida/vazia (★/☆) funcionam como o
rótulo da linha — não há necessidade de também soletrar "5 estrelas" em texto. Construído inteiramente em
bar() + renderizar().
Mapa de calor
from ascii_chart import mapa_calor
print(mapa_calor(matriz, ["Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"],
marcas_coluna=[(0, "0h"), (6, "6h"), (12, "12h"), (18, "18h")]))
matriz é linhas × colunas de intensidade numérica (por exemplo, 7 dias × 24 horas),
normalizada em toda a matriz (não por linha) para que o sombreamento seja
comparável entre linhas. marcas_coluna é [(índice_coluna, rótulo), ...] — sem
espaçamento automático, escolha posições que não colidirão para os rótulos que você passar.
Gráfico de linha / minigráfico
from ascii_chart import gráfico_linha, minigráfico
print(gráfico_linha([120, 135, 128, 150, 175, 190, 205, 198, 220],
altura=10, rótulos_x=("Jan", "Set")))
print(minigráfico([3, 5, 4, 8, 12, 10, 15, 14, 18]))
gráfico_linha desenha uma linha conectada com cantos de desenho de caixa (╭╮╰╯│─) —
não há caractere para uma inclinação intermediária arbitrária, então a largura é fixa
em len(valores) - 1 colunas (uma por segmento). rótulos_x, se fornecido, é
apenas (primeiro, último) impresso na linha do eixo — diga os pontos intermediários na
prosa ao redor, não tente rotular cada marca. minigráfico é uma
tendência de 8 níveis de linha única (▁▂▃▄▅▆▇█) para uma cláusula dentro de uma frase, não
um gráfico independente.
Venn (2 ou 3 conjuntos)
from ascii_chart import venn
diagrama, legenda = venn(["Saída de rodovia", "Grande loja", "Capacidade da rede"])
print(diagrama)
print(legenda)
Rasteriza círculos sobrepostos e preenche cada região com um dígito
(uma máscara de bits de qual(is) conjunto(s) ela pertence: 1, 2, 4 para os conjuntos simples,
3/5/6 para sobreposições aos pares, 7 para todos os três) em vez de tentar
encaixar texto de rótulo dentro de um limite curvo, o que não sobrevive à renderização
monoespaçada. A legenda (retornada separadamente, imprima ambas) mapeia cada dígito
de volta à combinação de rótulo real, por exemplo, 7 = Saída de rodovia + Grande loja + Capacidade da rede <- todos eles. Isso foi originalmente acreditado impossível em
ASCII (um post inicial recorreu a um PNG headless do Chrome para seu Venn) — a
técnica de região de dígito resolve isso de forma limpa; não há mais um caso onde
Venn precise da alternativa PNG.
Mas execute-o através do teste de 2 segundos acima antes de recorrer a ele — a própria legenda de dígitos é a coisa que tornou um Venn real confuso para um leitor leigo. Um Venn ganha seu lugar quando o grau de sobreposição é a descoberta (por exemplo, "38% de suas avaliações de uma estrela também mencionam tempo de espera"). Quando a mensagem real é "você precisa de todos os três juntos, sem crédito parcial," um Venn é a ferramenta errada mesmo quando renderiza corretamente — texto simples faz esse trabalho sem legenda necessária, por exemplo:
Saída de rodovia + Grande loja + Capacidade da rede
perca um desses, pule o site
Pizza / rosca
from ascii_chart import pizza, rosca
diagrama, legenda = pizza([("Café", 40), ("Comida quente", 25), ("Bebidas", 20), ("Loteria", 15)], raio=9)
print(diagrama); print(legenda)
diagrama, legenda = rosca([("Café", 40), ("Comida quente", 25), ("Bebidas", 20), ("Loteria", 15)],
raio=9, texto_centro="247")
print(diagrama); print(legenda)
As fatias começam às 12 horas e vão no sentido horário (convenção padrão de pizza), cada
uma recebe um caractere de preenchimento distinto (█▓▒░▚▞▤▥, nunca repetido em uma fatia
adjacente para contagens típicas de 2–6 fatias) para que os limites permaneçam legíveis sem
cor. rosca é pizza com um centro oco — texto_centro coloca um
número/rótulo do título lá (por exemplo, uma contagem total), que é a única coisa que uma
rosca ASCII pode fazer que uma real geralmente desperdiça em espaço vazio. raio
está em unidades de linha; a função corrige internamente para a proporção do caractere
monoespaçado (constante ASPECTO compartilhada, também usada por venn()) para que o
resultado seja realmente redondo, não uma oval achatada — não ajuste manualmente
largura/altura separadamente, apenas passe raio.
Correção: ASPECTO estava codificado para um assumido 2.0