Gráficos ASCII
Gráficos con estilo de terminal construidos a partir de caracteres Unicode de bloque simples e incrustados como texto literal:
un bloque de código en Markdown, una etiqueta <pre> en HTML. Sin servicio de gráficos, sin pie de página "made with",
sin cuenta externa, y se renderizan de forma idéntica dondequiera que se renderice texto monoespaciado.
scripts/ascii_chart.py tiene una función por tipo de gráfico. Ejecuta python3 scripts/ascii_chart.py
para imprimir un ejemplo de cada tipo como verificación de cordura.
Dos cosas aquí se midieron en lugar de asumirse, y ambas estaban equivocadas al principio: en una ventana de 390px de teléfono, las filas de más de ~44 caracteres se cortan en el borde derecho, y un círculo "redondo" en monoespaciado necesita una corrección de aspecto horizontal de aproximadamente 2.49 (no 2.0). Consulta Dimensionamiento y la sección de Circular/ Dona.
Las plataformas de boletines difieren en cómo pegan bloques de código. Antes de confiar en una, pega un gráfico de prueba en un borrador real y verifica las representaciones en web, aplicación y correo electrónico.
Conjunto de caracteres
█▓▒░(U+2588, U+2593, U+2592, U+2591 — sombra completa/oscura/media/clara) ) — densidad de relleno, de "más" a "menos". Las barras usan solo█/░(relleno vs. pista); circular/dona usan los cuatro (más▚/▞/▤/▥si hay más de 4 porciones) como rellenos distintos por porción; el mapa de calor usa los cuatro más·(punto medio, no un espacio en blanco) para el nivel más bajo, de modo que una celda "tranquila" aún se lea como parte intencional de la cuadrícula, no como un hueco.- dígitos
1–7— códigos de región de Venn (una máscara de bits de qué conjunto(s) pertenece una región — ver Venn abajo) +— indicador abierto en una barra de rango que podría ejecutarse más alto que el techo mostrado (no estires la barra para fingir un número que no tienes)┃— marca de objetivo de gráfico de viñetas, dibujada sobre el relleno/sombreado que sea debajo para que siempre sea visible└─┘— paréntesis de brecha, que se extiende entre dos extremos de barras★/☆— estrella llena/vacía, que también sirve como etiqueta de fila del histograma de estrellas- dibujo de cajas (
─│╭╮╰╯) — conectores de gráficos de línea y líneas de tendencia ●— marcador de punto de dispersión
Sin color, en ninguna parte. Ese es el medio, no una limitación a superar: un bloque de código pegado de Substack son caracteres literales, nada más sobrevive (sin CSS en línea, sin tramos con estilo). Cualquier distinción que un gráfico necesite hacer reside en el carácter/dígito/leyenda, no en un canal de color no disponible.
Menú de tipos de gráficos
| Trabajo | Función | Usar para |
|---|---|---|
| Comparación de magnitudes, varias categorías, todas comienzan en 0 | bar(valor, max_valor, ancho) + render() | Llenado de gasolina (5 min) vs. carga rápida DC (45 min) |
| Un rango mínimo-máximo, una categoría | range_bar(inicio, fin, max_escala, ancho, abierto=) + render() | "$100k–$300k+ instalado" |
| Dos valores, antes/después | Texto plano: f"{inicio} → {fin}" | "Por mayor $220 → barra $650" — una barra no agrega nada; la flecha es el gráfico |
| Una medida vs. una banda de referencia + un objetivo | bullet(valor, max_escala, objetivo=, referencia=) + render() | "Tu calificación: 4.2, banda normal 3.8–4.5, objetivo 4.7" |
| Llamar explícitamente a la brecha entre dos barras | gap_bracket(a, b, max_escala, ancho, etiqueta=) a través de render(paréntesis=) | "Tú vs. mediana del mercado" — el movimiento característico del informe |
| Una distribución de calificación 1–5 (o similar) | star_histogram(conteos) | Desglose de estrellas de Google/Yelp, una fila por conteo de estrellas |
| Dos ejes categóricos, sombreados por intensidad | heatmap(matriz, etiquetas_filas, marcas_columnas=) | Tráfico peatonal por día × hora — "cuándo estás ocupado/muerto" |
| Tendencia a lo largo del tiempo, una serie | line_chart(valores, altura, etiquetas_x=) | Ingresos mensuales, conteo de reseñas durante un año |
| Tendencia, en línea dentro de una oración | sparkline(valores) | "Reseñas por mes: ▂▃▅▆█▇▅▃, hasta 3x desde marzo" |
| 2 o 3 conjuntos con nombre, donde el grado de superposición es en sí mismo el hallazgo | venn(etiquetas) | "El 38% de tus reseñas de una estrella también mencionan el tiempo de espera" — el tamaño de la superposición es el punto |
| Partes de un todo, una instantánea | pie(porciones, radio) | Mezcla de ingresos: café / comida caliente / bebidas / lotería |
| Partes de un todo, con un total destacado | donut(porciones, radio, texto_centro=) | Igual, con "247 reseñas" sentado en el centro hueco |
| Dos variables numéricas, ¿hay una relación? | scatter(puntos, ancho, altura) | Precio vs. calificación entre competidores |
| Igual, más "y aquí está la tendencia" | scatter_with_trend(puntos, ancho, altura) | Igual, con una línea ajustada que prueba la correlación |
Si nada en esta tabla encaja, detente y piensa en lugar de forzar una forma que no encaja: un gráfico que es técnicamente renderizable pero confuso es peor que recurrir a la canalización PNG de Chrome sin cabeza.
La prueba de 2 segundos para gráficos
Un gráfico responde al mismo estándar que una buena oración: ser entendido por un lector cansado en su teléfono, a primera vista, sin releer, sin decodificar. Aplica la prueba antes de elegir un tipo de gráfico, no después de construir uno.
La prueba de 2 segundos: ¿podría el lector previsto — un propietario de pequeña empresa, no un analista de datos — decir qué significa este gráfico en 2 segundos, sin leer una leyenda primero? Si entender incluso una región o porción requiere decodificar una leyenda primero, eso es el equivalente visual de jerga: técnicamente preciso, pero obliga al lector a trabajar en lugar de atraerlo.
Atrapado en un problema real, por el editor leyendo la publicación renderizada en frío:
- El diagrama de máscara de bits de dígitos de 3 conjuntos de Venn requería leer una leyenda de 7 líneas antes de que cualquier región significara algo. El editor: no lo entendieron, por lo que un propietario de negocio regular tampoco lo haría. El hecho subyacente — "necesitas los tres, no solo uno" — es una oración, y la lista numerada justo después del diagrama ya lo decía en inglés sencillo. El diagrama agregó costo de decodificación sin agregar información que la prosa ya no llevara más claramente. Arreglo: elimínalo, deja que la oración + lista hagan el trabajo.
- La barra de rango de costos del cargador rápido DC (una sola barra, sin eje, sin punto de comparación) no tenía nada con qué compararse. El valor de una barra proviene de mostrar una magnitud relativa a algo — una escala, otra barra, un punto de referencia. Una barra solitaria sin punto de referencia es decorativa, no informativa. El número que ilustraba ($100k–$300k+) ya estaba establecido claramente una oración antes. Arreglo: elimínalo también.
Reglas que siguen:
- Un gráfico que necesita su propia leyenda para ser leído es una señal de advertencia — no solo para Venn. Un mapa de calor con ejes sin etiquetar, o un circular con más porciones de las que un lector puede retener en su cabeza, falla de la misma manera.
- Si una oración simple expresa el hecho tan claramente como lo haría el gráfico, omite el gráfico. Mismo instinto que intercambiar "varios/muchos/varios" por el conteo real en prosa: recurre a lo visual porque una comparación o tendencia es el punto, no solo porque los datos tienen una forma.
- Cada barra necesita un eje (
render(..., eje=)) o una segunda barra en la misma escala para comparar. Una sola barra sin ninguno de los dos no transmite nada — la misma falla que una oración en voz pasiva sin sujeto real. Pero una vez que eso está satisfecho, elimina el eje si solo repetiría un número que ya está en una etiqueta de valor por fila — una barra de gas-vs-carga eliminó su línea de eje0 ... 45 minpor exactamente esta razón: ambas filas ya llevan su propio valor ("5 min", "45 min"), por lo que el eje solo repetía "45 min" una segunda vez. Mismo principio que cortar una cláusula redundante de una oración. - No grafiques un número que ya es una oración clara. "$100k–$300k+ instalado" es máximamente claro como texto; graficarlo no revela una comparación, tendencia o superposición, solo agrega un paso de decodificación sin recompensa.
- Lee el gráfico terminado en frío, como lo hará el lector real — un propietario de estación de gasolina en su teléfono, no la persona que acaba de construirlo y ya sabe lo que se supone que debe decir.
Flujo de trabajo
Usa scripts/ascii_chart.py en lugar de alinear caracteres manualmente o
rasterizar geometría tú mismo — las matemáticas de columnas y círculos/ángulos son
exactamente el tipo de cosa que es fácil de hacer sutilmente mal a ojo y solo
aparece como una desalineación de un carácter o un círculo torcido después del
hecho. Ejecuta python3 scripts/ascii_chart.py en cualquier momento para imprimir un
ejemplo de cada tipo de gráfico como verificación de cordura.
Barra / barra de rango
from ascii_chart import bar, range_bar, render
filas = [
("Llenado de gasolina", bar(5, 45, 24), "5 min"),
("Carga rápida DC", bar(45, 45, 24), "45 min"),
]
print(render(filas, eje=("0", "45 min")))
fila = ("Cargador rápido DC", range_bar(100, 300, 300, 24, abierto=True), "$100k–$300k+")
print(render([fila]))
render() toma tuplas (etiqueta, cadena_barra, cadena_valor) y maneja toda
la alineación de columnas — pasa tu propio cadena_valor preformateado (moneda,
unidades, lo que necesite la copia); la función solo maneja geometría.
Gráfico de viñetas
from ascii_chart import bullet, render
filas = [("Calificación promedio", bullet(4.2, 5.0, objetivo=4.7, referencia=(3.8, 4.5), ancho=24), "4.2 (objetivo 4.7)")]
print(render(filas, eje=("0", "5.0")))
Una barra sólida para el valor medido, dibujada sobre una banda de referencia
sombreada opcional (el rango "normal", ▒) con una marca objetivo opcional
(┃, siempre dibujada al final para que permanezca visible sobre cualquier relleno/sombreado debajo
). Responde "¿estoy dentro de lo normal y qué tan lejos del objetivo?" de un vistazo —
todavía hay solo una barra real (la medida); la banda es contexto.
Paréntesis de brecha
from ascii_chart import bar, gap_bracket, render
filas = [("Tú", bar(42000, 60000, 24), "$42,000"), ("Mediana del mercado", bar(58000, 60000, 24), "$58,000")]
print(render(filas, paréntesis=gap_bracket(42000, 58000, 60000, 24)))
Un paréntesis └─┘ que se extiende entre dos extremos de barras, con el tamaño de la brecha
centrado dentro de él (autoformateado a partir de los dos valores, o pasa etiqueta=
para tu propio texto). Usa el mismo max_escala/ancho para las barras y
el paréntesis, o el tramo no se alineará. Esto es "tú vs. el mercado,"
hecho explícito en lugar de dejar que el lector lo calcule a ojo.
Histograma de estrellas
from ascii_chart import star_histogram
print(star_histogram([(5, 61), (4, 22), (3, 9), (2, 4), (1, 4)]))
conteos es [(estrellas, conteo), ...] en el orden que quieras que se imprima
(generalmente 5 a 1). Los glifos de estrella llena/vacía (★/☆) también sirven como
etiqueta de fila — no es necesario deletrear también "5 estrellas" en texto. Construido
completamente en bar() + render().
Mapa de calor
from ascii_chart import heatmap
print(heatmap(matriz, ["Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"],
marcas_columnas=[(0, "12am"), (6, "6am"), (12, "12pm"), (18, "6pm")]))
matriz es filas × columnas de intensidad numérica (por ejemplo, 7 días × 24 horas),
normalizado a través de la matriz completa (no por fila) para que el sombreado sea
comparable entre filas. marcas_columnas es [(índice_columna, etiqueta), ...] — sin
autoespaciado, elige posiciones que no choquen para las etiquetas que pases.
Gráfico de línea / chispa
from ascii_chart import line_chart, sparkline
print(line_chart([120, 135, 128, 150, 175, 190, 205, 198, 220],
altura=10, etiquetas_x=("Ene", "Sep")))
print(sparkline([3, 5, 4, 8, 12, 10, 15, 14, 18]))
line_chart dibuja una línea conectada con esquinas de dibujo de cajas (╭╮╰╯│─) —
no hay carácter para una pendiente intermedia arbitraria, por lo que el ancho es fijo
en len(valores) - 1 columnas (una por segmento). etiquetas_x, si se proporciona, es
solo (primero, último) impreso en la fila del eje — di los puntos intermedios en
la prosa circundante, no intentes etiquetar cada marca. sparkline es una
tendencia de una fila de 8 niveles (▁▂▃▄▅▆▇█) para una cláusula dentro de una oración, no
un gráfico independiente.
Venn (2 o 3 conjuntos)
from ascii_chart import venn
diagrama, leyenda = venn(["Salida de autopista", "Tienda grande", "Capacidad de red"])
print(diagrama)
print(leyenda)
Rasteriza círculos superpuestos y llena cada región con un dígito
(una máscara de bits de qué conjunto(s) pertenece: 1, 2, 4 para conjuntos simples,
3/5/6 para superposiciones por pares, 7 para los tres) en lugar de intentar
encajar texto de etiqueta dentro de un límite curvo, que no sobrevive a la representación
monoespaciada. La leyenda (devuelta por separado, imprime ambos) asigna cada
dígito de vuelta a la combinación de etiquetas real, por ejemplo, 7 = Salida de autopista + Tienda grande + Capacidad de red <- todos ellos. Esto se creyó originalmente imposible
en ASCII (una publicación temprana recurrió a un PNG sin cabeza de Chrome para su Venn) — la
técnica de región de dígitos lo resuelve limpiamente; ya no hay un caso donde
Venn necesite la alternativa PNG.
Pero pásalo por la prueba de 2 segundos antes de usarlo — la leyenda de dígitos en sí misma es lo que hizo que un Venn real fuera confuso para un lector no experto. Un Venn se gana su lugar cuando el grado de superposición es el hallazgo (por ejemplo, "El 38% de tus reseñas de una estrella también mencionan el tiempo de espera"). Cuando el mensaje real es "necesitas los tres juntos, sin crédito parcial," un Venn es la herramienta equivocada incluso cuando se renderiza correctamente — el texto plano hace ese trabajo sin leyenda requerida, por ejemplo:
Salida de autopista + Tienda grande + Capacidad de red
falla uno de estos, omite el sitio
Circular / Dona
from ascii_chart import pie, donut
diagrama, leyenda = pie([("Café", 40), ("Comida caliente", 25), ("Bebidas", 20), ("Lotería", 15)], radio=9)
print(diagrama); print(leyenda)
diagrama, leyenda = donut([("Café", 40), ("Comida caliente", 25), ("Bebidas", 20), ("Lotería", 15)],
radio=9, texto_centro="247")
print(diagrama); print(leyenda)
Las porciones comienzan a las 12 en punto y van en sentido horario (convención estándar de circular), cada
una obtiene un carácter de relleno distinto (█▓▒░▚▞▤▥, nunca repetido en una porción
adyacente para conteos típicos de 2 a 6 porciones) para que los límites permanezcan legibles sin
color. donut es pie con un centro hueco — texto_centro pone un
número/etiqueta destacado allí (por ejemplo, un conteo total), que es lo único que una dona
ASCII puede hacer que una real generalmente desperdicia en espacio vacío. radio está
en unidades de fila; la función corrige internamente la relación de aspecto del carácter monoespaciado
(constante ASPECT compartida, también usada por venn()) para que el resultado
sea realmente redondo, no un óvalo aplanado — no ajustes manualmente el ancho/altura por separado,
solo pasa radio.
Corrección: ASPECT estaba codificado como 2.0 asumido y nunca había sido verificado contra un
círculo renderizado real en una terminal. Cuando finalmente se verificó, el error era obvio: un radio de 9
producía una elipse de 18 caracteres de ancho por 9 filas de alto, que es un círculo aplanado porque
los caracteres monoespaciados son más altos que anchos. La relación de aspecto real para un círculo
que parece redondo en texto monoespaciado es aproximadamente 2.49 (derivada de la relación de
ancho de glifo a altura en una fuente de terminal típica como Menlo o SF Mono).
Importancia: sin esta corrección, los círculos se ven aplanados y las proporciones de las porciones
están sutilmente sesgadas. Con ella, un circular con radio=9 produce
18 caracteres de ancho por 9 filas de alto, y los límites de las porciones están espaciados correctamente.
La misma constante ASPECT se aplica en venn() para la rasterización de círculos superpuestos.
Dispersión
from ascii_chart import scatter, scatter_with_trend
print(scatter(puntos, ancho=40, altura=15))
print(scatter_with_trend(puntos, ancho=40, altura=15))
puntos es una lista de tuplas (x, y) (flotantes o enteros, normalizados internamente).
scatter_with_trend superpone una línea de regresión lineal (dibujada usando los conectores de línea y de esquina
─│╭╮╰╯ de line_chart). No se necesita ajuste de eje; la función escala automáticamente para
ajustar todos los puntos dentro del ancho/altura dados. Usa ● para marcadores de puntos.
Eje
Pasa una tupla eje=("izquierda", "derecha") a render() para mostrar una línea de eje entre
las barras y las etiquetas de valor. Sin ella, no se muestra ningún eje — no hay
predeterminado. Para una sola barra aislada sin nada con qué comparar, la banda del eje
esa menudo todo lo que la convierte de decorativa a informativa (pero ver la prueba de 2 segundos arriba
sobre cuándo eliminar el eje si las etiquetas de valor por fila ya transmiten
la escala). El eje en sí no se traduce; imprime las cadenas exactamente como se proporcionan: el
contenido de la copia decide el formato.
Dimensionamiento
Cada gráfico aquí se ha ajustado a mano para un ancho de 24 a 28 caracteres (la celda de la barra en sí, ecuaciones de la etiqueta y la columna de valor), porque en un diseño de boletín de respuesta en una ventana de 390px de teléfono típica, una fila más allá de ~44 caracteres se corta. Las celdas de la barra más anchas que ~30 se comen demasiado del búfer de etiqueta/valor a la izquierda y derecha. Las celdas más estrechas que ~20 no dejan suficiente resolución para diferencias sutiles entre barras.
La altura para line_chart es tu elección (pasa altura=). Para pie/donut, es 2 * radio
filas (la función corrige internamente la relación de aspecto del carácter). Para scatter, es el
altura que pases.
No hay barra de desplazamiento horizontal. Ningún gráfico en este sistema debería forzar a un lector a desplazarse. Si estás graficando tantas categorías que no caben dentro de ~44 caracteres, el problema no es que la barra sea demasiado larga, es que hay demasiados puntos de datos para este medio. Reduce: agrupa, muestra solo los 5 primeros, o envía al lector a un panel con un enlace.
Configuración / dependencias
Ninguna. scripts/ascii_chart.py es un solo archivo, biblioteca estándar de Python 3.x solamente, sin
pip install, sin requirements.txt, sin dependencias externas en absoluto. Copia el
archivo a tu proyecto, impórtalo y ejecuta funciones. Si tu agente AI puede crear o editar un
archivo .py, puede usar este sistema de gráficos.
Contexto del sistema
Añade a tu ~/.cursorrules, INSTRUCTIONS.md o indicación de personalización de Claude:
Cuando muestres datos numéricos en un bloque de código o etiqueta
<pre>, dibuja un gráfico ASCII usandoscripts/ascii_chart.pydel archivo adjunto. No envíes texto sin procesar Imagen PNG o SVG. Usabar()/range_bar()para comparaciones de magnitud,bullet()para medida contra banda de referencia/objetivo,gap_bracket()para brecha explícita entre dos elementos,star_histogram()para desgloses de calificación,heatmap()para intensidad de dos ejes,line_chart()/sparkline()para tendencias de una serie,venn()para superposición de conjuntos (solo si el grado de superposición es el punto y pasa la prueba de 2 segundos),pie()/donut()para partes de un todo, yscatter()/scatter_with_trend()para correlación de dos variables. Siempre aplica la prueba de 2 segundos: si el lector objetivo no entiende el gráfico sin una leyenda, usa texto plano en su lugar. Prefiere 2–4 puntos de datos sobre <20. Ningún gráfico debe exceder ~44 caracteres de ancho (sin desplazamiento horizontal). No hay dependencias externas, sin color, sin imágenes. El gráfico debe ser comprensible de un vistazo, no un acertijo para ser resuelto.
Qué evitar
No hay matplotlib
No importes matplotlib, seaborn, plotly o cualquier otro backend de gráficos con dependencias. La especificación completa es: un archivo de biblioteca estándar que un agente puede escribir y el lector puede ver como texto.
No hay SVG ni PNG
Una vez que el gráfico es una imagen, deja de ser incrustable en línea en un bloque de código de boletín. Las imágenes requieren alojamiento, fallan silenciosamente en correo electrónico y no se renderizan en absoluto en algunas aplicaciones de chat.
Sin leyenda significa "sin necesidad de ella"
Una leyenda que traduzca un código de región a su significado (como el dígito de Venn a etiquetas de conjuntos) está bien. Una leyenda que el lector debe decodificar antes de que el gráfico tenga sentido es una falla: si el lector tiene que leer una tabla de mapeo para entender qué significa una región de Venn, el gráfico ha perdido su propósito. Usa la prosa circundante para establecer lo que muestra el gráfico; el gráfico debe ser reconocible de inmediato. El caso de Venn capturado en la prueba de 2 segundos arriba es exactamente esta falla.
Sin color
El medio es texto monoespaciado sin formato. Sin resaltado de sintaxis, sin tramos CSS, sin imágenes sediciosas en celdas de tabla. Los gráficos deben usar solo caracteres; cualquier distinción que un gráfico necesite hacer debe ser en forma de carácter o posición de carácter.
Referencia rápida
| Tipo | Función | Importación |
|---|---|---|
| Barra | bar(valor, max_valor, ancho) | from ascii_chart import bar |
| Rango | range_bar(inicio, fin, max_escala, ancho, abierto=) | from ascii_chart import range_bar |
| Viñeta | bullet(valor, max_escala, objetivo=, referencia=) | from ascii_chart import bullet |
| Brecha | gap_bracket(a, b, max_escala, ancho, etiqueta=) | from ascii_chart import gap_bracket |
| Histograma de estrellas | star_histogram(conteos) | from ascii_chart import star_histogram |
| Mapa de calor | heatmap(matriz, etiquetas_filas, marcas_columnas=) | from ascii_chart import heatmap |
| Línea | line_chart(valores, altura, etiquetas_x=) | from ascii_chart import line_chart |
| Chispa | sparkline(valores) | from ascii_chart import sparkline |
| Venn | venn(etiquetas) | from ascii_chart import venn |
| Circular | pie(porciones, radio) | from ascii_chart import pie |
| Dona | donut(porciones, radio, texto_centro=) | from ascii_chart import donut |
| Dispersión | scatter(puntos, ancho, altura) | from ascii_chart import scatter |
| Dispersión + tendencia | scatter_with_trend(puntos, ancho, altura) | from ascii_chart import scatter_with_trend |
| Render | render(filas, eje=, paréntesis=) | from ascii_chart import render |
Ejecuta python3 scripts/ascii_chart.py en cualquier momento para imprimir un ejemplo de
cada tipo.