Graphiques ASCII
Des graphiques de style terminal construits à partir de caractères Unicode block purs et intégrés sous forme de texte littéral :
un bloc de code délimité en Markdown, une balise <pre> en HTML. Aucun service de graphiques, aucun pied de page « créé avec »,
aucun compte externe, et ils s'affichent à l'identique partout où le monospace s'affiche.
scripts/ascii_chart.py contient une fonction par type de graphique. Exécutez python3 scripts/ascii_chart.py
pour imprimer un exemple de chaque type à titre de vérification de cohérence.
Deux choses ici ont été mesurées plutôt que supposées, et elles s'avéraient toutes deux erronées au départ : sur un viewport de téléphone de 390 px, les lignes de plus de ~44 caractères sont tronquées sur le bord droit, et un cercle « rond » en monospace nécessite une correction d'aspect horizontal d'environ 2,49 (et non 2,0). Voir la section Dimensionnement et Pie/donut.
Les plateformes de newsletters diffèrent dans la manière dont elles collent les blocs de code. Avant de vous fier à l'une d'elles, collez un graphique de test dans un véritable brouillon et vérifiez les rendus web, application et email.
Jeu de caractères
█▓▒░(U+2588, U+2593, U+2592, U+2591 — ombre pleine/foncée/moyenne/claire) — densité de remplissage, de « plus » à « moins ». Les barres utilisent uniquement█/░(rempli vs piste) ; les camemberts/donuts utilisent les quatre (plus▚/▞/▤/▥s'il y a plus de 4 parts) en tant que remplissages distincts par part ; la heatmap utilise les quatre plus·(point médian, pas un espace vide) pour le niveau le plus bas, de sorte qu'une cellule « calme » soit toujours perçue comme une partie intentionnelle de la grille, et non un trou.- les chiffres
1–7— codes de région de Venn (un masque de bits des ensemble(s) auxquels une région appartient — voir Venn ci-dessous) +— indicateur ouvert sur une barre de plage qui pourrait dépasser le plafond affiché (n'allongez pas la barre elle-même pour simuler un nombre que vous n'avez pas)┃— repère de cible pour le graphique bullet, dessiné par-dessus tout remplissage/ombrage en dessous, donc toujours visible└─┘— crochet d'écart, s'étendant entre deux extrémités de barre★/☆— étoile pleine/vide, servant également d'étiquette de ligne pour l'histogramme en étoiles- traits de cadre (
─│╭╮╰╯) — connecteurs pour graphiques linéaires et lignes de tendance ●— marqueur de point pour nuage de points
Pas de couleur, nulle part. C'est le support, pas une limitation à contourner — un bloc de code Substack collé est constitué de caractères littéraux, rien d'autre ne survit (pas de CSS en ligne, pas de spans stylisés). Toute distinction qu'un graphique doit faire réside dans le caractère/chiffre/légende, et non dans un canal de couleur indisponible.
Menu des types de graphiques
| Tâche | Fonction | Utilisation pour |
|---|---|---|
| Comparaison de grandeurs, plusieurs catégories, toutes commencent à 0 | bar(valeur, valeur_max, largeur) + render() | Plein d'essence (5 min) vs charge rapide DC (45 min) |
| Une plage min–max, une catégorie | range_bar(début, fin, échelle_max, largeur, ouvert=) + render() | « Installe entre 100k$ et 300k$+ » |
| Deux valeurs, avant/après | Texte brut : f"{début} → {fin}" | « Vente en gros 220$ → dégustation 650$ » — un graphique en barres n'apporte rien ; la flèche est le graphique |
| Une mesure par rapport à une bande de référence + une cible | bullet(valeur, échelle_max, cible=, référence=) + render() | « Votre note : 4,2, bande normale 3,8–4,5, cible 4,7 » |
| Mettre en évidence l'écart entre deux barres | gap_bracket(a, b, échelle_max, largeur, étiquette=) via render(crochet=) | « Vous vs médiane du marché » — la signature du rapport |
| Une distribution de notes de 1 à 5 (ou similaire) | star_histogram(compteurs) | Répartition des étoiles Google/Yelp, une ligne par nombre d'étoiles |
| Deux axes catégoriels, ombrés par intensité | heatmap(matrice, étiquettes_lignes, graduations_colonnes=) | Trafic piétonnier par jour × heure — « quand êtes-vous occupé/mort » |
| Tendance dans le temps, une série | line_chart(valeurs, hauteur, étiquettes_x=) | Chiffre d'affaires mensuel, nombre d'avis sur un an |
| Tendance, en ligne dans une phrase | sparkline(valeurs) | « Avis par mois : ▂▃▅▆█▇▅▃, en hausse de 3x depuis mars » |
| 2 ou 3 ensembles nommés, où le degré de chevauchement est lui-même la conclusion | venn(étiquettes) | « 38 % de vos avis à une étoile mentionnent également le temps d'attente » — la taille du chevauchement est le point |
| Parties d'un tout, un instantané | pie(parts, rayon) | Répartition du chiffre d'affaires : café / plats chauds / boissons / loterie |
| Parties d'un tout, avec un total en vedette | donut(parts, rayon, texte_centre=) | Idem, avec « 247 avis » au centre creux |
| Deux variables numériques, y a-t-il une relation | scatter(points, largeur, hauteur) | Prix vs note parmi les concurrents |
| Idem, plus « et voici la tendance » | scatter_with_trend(points, largeur, hauteur) | Idem, avec une ligne ajustée prouvant la corrélation |
Si rien dans ce tableau ne correspond, arrêtez-vous et réfléchissez plutôt que de forcer une forme qui ne convient pas — un graphique techniquement rendu mais déroutant est pire que de recourir au pipeline PNG sans tête Chrome.
Le test des 2 secondes pour les graphiques
Un graphique répond au même standard qu'une bonne phrase : être compris par un lecteur fatigué sur son téléphone, du premier coup d'œil, sans relecture, sans décodage. Appliquez le test avant de choisir un type de graphique, pas après en avoir construit un.
Le test des 2 secondes : le lecteur visé — un propriétaire de petite entreprise, pas un analyste de données — pourrait-il dire ce que ce graphique signifie en 2 secondes, sans lire d'abord une légende ? Si comprendre une seule région ou part nécessite de décoder une légende en premier, c'est l'équivalent visuel du jargon : techniquement précis, mais cela oblige le lecteur à travailler au lieu de le faire avancer.
Pris sur un vrai problème, par le rédacteur lisant le message rendu à froid :
- Le diagramme de Venn à 3 ensembles avec masque de bits chiffré nécessitait de lire une légende de 7 lignes avant qu'une région n'ait un sens. Le rédacteur : ils ne l'ont pas compris, donc un propriétaire d'entreprise ordinaire non plus. Le fait sous-jacent — « vous avez besoin de ces trois éléments, pas d'un seul » — tient en une phrase, et la liste numérotée juste après le diagramme le disait déjà en langage clair. Le diagramme a ajouté un coût de décodage sans ajouter d'informations que la prose ne portait pas déjà plus clairement. Correction : supprimez-le, laissez la phrase + la liste faire le travail.
- La barre de plage des coûts du chargeur rapide DC (une seule barre, sans axe, sans point de comparaison) n'avait rien à quoi se comparer. La valeur d'une barre vient du fait de montrer une grandeur relative à quelque chose — une échelle, une autre barre, une référence. Une barre isolée sans point de référence est décorative, pas informative. Le nombre qu'elle illustrait (100k$–300k$+) était déjà énoncé clairement une phrase plus tôt. Correction : supprimez-la aussi.
Règles qui en découlent :
- Un graphique qui a besoin de sa propre légende pour être lu est un signal d'alarme — pas seulement pour Venn. Une heatmap avec des axes non étiquetés, ou un camembert avec plus de parts que le lecteur ne peut en retenir mentalement, échoue de la même manière.
- Si une phrase simple énonce le fait aussi clairement que le graphique le ferait, sautez le graphique. Même réflexe que de remplacer « plusieurs/beaucoup/divers » par le chiffre réel dans la prose : utilisez un visuel parce qu'une comparaison ou une tendance est le point, pas simplement parce que les données ont une forme.
- Chaque barre a besoin d'un axe (
render(..., axe=)) ou d'une deuxième barre sur la même échelle pour se comparer. Une seule barre sans l'un ni l'autre ne transmet rien — la même défaillance qu'une phrase en voix passive sans sujet réel. Mais une fois cela satisfait, supprimez l'axe s'il ne ferait que répéter un nombre déjà présent dans une étiquette de valeur par ligne — une barre essence-vs-charge a supprimé sa ligne d'axe0 ... 45 minpour exactement cette raison : les deux lignes portent déjà leur propre valeur (« 5 min », « 45 min »), donc l'axe ne faisait que répéter « 45 min » une deuxième fois. Même principe que de couper une clause redondante d'une phrase. - Ne graphiquez pas un nombre qui tient déjà en une phrase claire. « Installe entre 100k$–300k$+ » est parfaitement clair en texte ; le graphiquer ne révèle pas une comparaison, une tendance ou un chevauchement, il ajoute simplement une étape de décodage sans aucun gain.
- Lisez le graphique fini à froid, comme le fera le vrai lecteur — un propriétaire de station-service sur son téléphone, pas la personne qui vient de le construire et sait déjà ce qu'il est censé dire.
Flux de travail
Utilisez scripts/ascii_chart.py plutôt que d'aligner manuellement les caractères ou de
rasteriser la géométrie vous-même — les mathématiques de colonnes et de cercles/angles sont exactement le genre de choses qu'il est facile de faire de manière subtilement erronée à l'œil et qui ne se manifestent que sous la forme d'un décalage d'un caractère ou d'un cercle asymétrique après coup. Exécutez python3 scripts/ascii_chart.py à tout moment pour imprimer un exemple de chaque type de graphique à titre de vérification de cohérence.
Barre / barre de plage
from ascii_chart import bar, range_bar, render
lignes = [
("Plein d'essence", bar(5, 45, 24), "5 min"),
("Charge rapide DC", bar(45, 45, 24), "45 min"),
]
print(render(lignes, axe=("0", "45 min")))
ligne = ("Chargeur rapide DC", range_bar(100, 300, 300, 24, ouvert=True), "100k$–300k$+")
print(render([ligne]))
render() prend des tuples (étiquette, chaîne_barre, chaîne_valeur) et gère tout l'alignement des colonnes — passez votre propre chaîne_valeur pré-formatée (devise, unités, tout ce dont la copie a besoin) ; la fonction ne gère que la géométrie.
Graphique bullet
from ascii_chart import bullet, render
lignes = [("Note moyenne", bullet(4.2, 5.0, cible=4.7, référence=(3.8, 4.5, largeur=24)), "4,2 (cible 4,7)")]
print(render(lignes, axe=("0", "5,0")))
Une barre pleine pour la valeur mesurée, dessinée par-dessus une bande de référence ombrée optionnelle (la plage « normale », ▒) avec un repère de cible optionnel (┃, toujours dessiné en dernier pour rester visible par-dessus tout remplissage/ombrage en dessous). Répond à « suis-je dans la normale, et à quelle distance de la cible » en un coup d'œil — il n'y a toujours qu'une seule vraie barre (la mesure) ; la bande est le contexte.
Crochet d'écart
from ascii_chart import bar, gap_bracket, render
lignes = [("Vous", bar(42000, 60000, 24), "42 000$"), ("Médiane du marché", bar(58000, 60000, 24), "58 000$")]
print(render(lignes, crochet=gap_bracket(42000, 58000, 60000, 24)))
Un crochet └─┘ s'étendant entre deux extrémités de barre, avec la taille de l'écart centrée à l'intérieur (formatée automatiquement à partir des deux valeurs, ou passez étiquette= pour votre propre texte). Utilisez le même échelle_max/largeur pour les barres et le crochet, sinon la portée ne s'alignera pas. C'est « vous vs. le marché », rendu explicite au lieu d'être laissé à l'appréciation du lecteur.
Histogramme en étoiles
from ascii_chart import star_histogram
print(star_histogram([(5, 61), (4, 22), (3, 9), (2, 4), (1, 4)]))
compteurs est [(étoiles, compteur), ...] dans l'ordre que vous souhaitez imprimer (généralement 5 à 1). Les glyphes d'étoile pleine/vide (★/☆) servent également d'étiquette de ligne — pas besoin d'écrire « 5 étoiles » en texte. Entièrement construit sur bar() + render().
Heatmap
from ascii_chart import heatmap
print(heatmap(matrice, ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"],
graduations_colonnes=[(0, "00h"), (6, "6h"), (12, "12h"), (18, "18h")]))
matrice est lignes × colonnes d'intensité numérique (par ex. 7 jours × 24 heures), normalisée sur l'ensemble de la matrice (pas par ligne) pour que l'ombrage soit comparable entre les lignes. graduations_colonnes est [(indice_colonne, étiquette), ...] — pas d'espacement automatique, choisissez des positions qui n'entreront pas en conflit pour les étiquettes que vous passez.
Graphique linéaire / sparkline
from ascii_chart import line_chart, sparkline
print(line_chart([120, 135, 128, 150, 175, 190, 205, 198, 220],
hauteur=10, étiquettes_x=("Jan", "Sep")))
print(sparkline([3, 5, 4, 8, 12, 10, 15, 14, 18]))
line_chart dessine une ligne connectée avec des coins de traits de cadre (╭╮╰╯│─) — il n'y a pas de caractère pour une pente intermédiaire arbitraire, donc la largeur est fixée à len(valeurs) - 1 colonnes (une par segment). étiquettes_x, si fourni, est simplement (premier, dernier) imprimé sur la ligne d'axe — dites les points intermédiaires dans la prose environnante, n'essayez pas d'étiqueter chaque graduation. sparkline est une tendance à 8 niveaux sur une seule ligne (▁▂▃▄▅▆▇█) pour une clause dans une phrase, pas un graphique autonome.
Venn (2 ou 3 ensembles)
from ascii_chart import venn
diagramme, légende = venn(["Sortie d'autoroute", "Grand magasin", "Capacité du réseau"])
print(diagramme)
print(légende)
Rastérise des cercles qui se chevauchent et remplit chaque région avec un chiffre
(un masque de bits des ensemble(s) auxquels elle appartient : 1, 2, 4 pour les ensembles simples,
3/5/6 pour les chevauchements par paires, 7 pour les trois) plutôt que d'essayer de faire tenir le texte de l'étiquette à l'intérieur d'une limite courbe, ce qui ne survit pas au rendu monospace. La légende (renvoyée séparément, imprimez les deux) mappe chaque chiffre
à la combinaison d'étiquettes réelle, par ex. 7 = Sortie d'autoroute + Grand magasin + Capacité du réseau <- tous. Cela était initialement considéré comme impossible en ASCII
(un premier article s'est rabattu sur un PNG sans tête Chrome pour son Venn) — la technique de la région chiffrée le résout proprement ; il n'y a plus de cas où Venn a besoin de la solution de repli PNG.
Mais passez-le au test des 2 secondes ci-dessus avant de l'utiliser — la légende des chiffres elle-même est ce qui a rendu un vrai Venn déroutant pour un lecteur non initié. Un Venn mérite sa place lorsque le degré de chevauchement est la conclusion (par ex. « 38 % de vos avis à une étoile mentionnent également le temps d'attente »). Lorsque le vrai message est « vous avez besoin des trois ensemble, pas de crédit partiel », un Venn est le mauvais outil même s'il se rend correctement — le texte brut fait ce travail sans légende requise, par ex. :
Sortie d'autoroute + Grand magasin + Capacité du réseau
manquez l'un d'eux, passez le site
Camembert / Donut
from ascii_chart import pie, donut
diagramme, légende = pie([("Café", 40), ("Plats chauds", 25), ("Boissons", 20), ("Loterie", 15)], rayon=9)
print(diagramme); print(légende)
diagramme, légende = donut([("Café", 40), ("Plats chauds", 25), ("Boissons", 20), ("Loterie", 15)],
rayon=9, texte_centre="247")
print(diagramme); print(légende)
Les parts commencent à 12 heures et vont dans le sens des aiguilles d'une montre (convention standard du camembert), chacune reçoit un caractère de remplissage distinct (█▓▒░▚▞▤▥, jamais répété sur une part adjacente pour des totaux de 2 à 6 parts) afin que les limites restent lisibles sans couleur. donut est pie avec un centre creux — texte_centre met un nombre/étiquette en vedette (par ex. un total), ce qui est la seule chose qu'un donut ASCII peut faire qu'un vrai gaspillage dans un espace vide. rayon est en unités de lignes ; la fonction corrige en interne le ratio d'aspect des caractères monospace (constante ASPECT partagée, également utilisée par venn()) pour que le résultat soit réellement rond, pas un ovale aplati — n'ajustez pas manuellement la largeur/hauteur séparément, passez simplement rayon.
Correction : ASPECT était codé en dur à une valeur supposée 2.0
et n'avait jamais été testé sur des formes circulaires réelles. Un test de validation a révélé que le ratio d'aspect effectif des caractères monospace dans la plupart des terminaux et environnements Markdown est d'environ 2.49. Cette correction garantit que les camemberts, donuts et diagrammes de Venn sont visuellement ronds, pas des ovales écrasés. Si le vôtre semble encore un peu aplati, ajustez ASPECT manuellement dans le script ou passez un facteur personnalisé via le paramètre aspect=. Notez que le ratio d'aspect peut varier légèrement entre les terminaux et les polices — testez toujours votre sortie finale dans l'environnement où elle sera lue.
Nuage de points
from ascii_chart import scatter, scatter_with_trend
points = [(1, 2), (2, 3), (3, 5), (4, 4), (5, 6), (6, 5), (7, 7), (8, 6), (9, 8), (10, 7)]
print(scatter(points, largeur=20, hauteur=8))
print(scatter_with_trend(points, largeur=20, hauteur=8))
scatter place des marqueurs ● à des coordonnées (x, y) dans une grille de caractères. Les axes sont implicites — les étiquettes ne sont pas générées automatiquement, décrivez les axes dans la prose environnante. scatter_with_trend ajoute une ligne de tendance ajustée (régression linéaire par moindres carrés) tracée avec des caractères ─│╭╮╰╯ et marquée avec un ● à chaque point de données. La ligne de tendance est tracée par-dessus les points, donc les deux sont visibles s'ils se chevauchent.
Contributeurs
Génial par Judea et Nicolasmean.
Licence
ISC