ASCII-Diagramme
Terminal-artige Diagramme, die aus einfachen Unicode-Blockzeichen erstellt und als wörtlicher Text eingebettet werden:
ein Codeblock in Markdown, ein <pre>-Tag in HTML. Kein Diagrammdienst, keine „erstellt mit“-
Fußzeile, kein externes Konto und sie werden überall dort identisch gerendert, wo Monospace rendert.
scripts/ascii_chart.py hat eine Funktion pro Diagrammtyp. Führe python3 scripts/ascii_chart.py
aus, um ein Beispiel jedes Typs als Plausibilitätsprüfung zu drucken.
Zwei Dinge hier wurden gemessen und nicht angenommen, und beide waren anfangs falsch: bei einem 390px- Telefon-Viewport schneiden Zeilen, die länger als ~44 Zeichen sind, am rechten Rand ab, und ein „runder“ Kreis in Monospace benötigt eine horizontale Seitenkorrektur von etwa 2,49 (nicht 2,0). Siehe Größenanpassung und den Abschnitt zu Torte/Ring.
Newsletter-Plattformen unterscheiden sich darin, wie sie Codeblöcke einfügen. Bevor du dich auf eine verlässt, füge ein Testdiagramm in einen echten Entwurf ein und überprüfe die Darstellung im Web, in der App und in E-Mails.
Zeichensatz
█▓▒░(U+2588, U+2593, U+2592, U+2591 — Voll-/dunkel/mittel/hell Schattierung) — Fülldichte, von „am meisten“ bis „am wenigsten“. Balken verwenden nur█/░(gefüllt vs. Spur); Torte/Ring verwenden alle vier (plus▚/▞/▤/▥bei mehr als 4 Segmenten) als unterschiedliche Füllungen pro Segment; die Heatmap verwendet alle vier plus·(Mittelpunkt, kein Leerzeichen) für die niedrigste Stufe, damit eine „ruhige“ Zelle immer noch als beabsichtigter Teil des Rasters gelesen wird, nicht als Lücke.- Ziffern
1–7— Venn-Regionscodes (eine Bitmaske, zu welcher(n) Menge(n) eine Region gehört — siehe Venn unten) +— offenes Ende an einem Bereichsbalken, der höher als die angezeigte Obergrenze laufen könnte (den Balken selbst nicht strecken, um eine Zahl vorzutäuschen, die du nicht hast)┃— Zielmarkierung für Aufzählungspunkt-Diagramme, die über jede darunter liegende Füllung/Schattierung gezeichnet wird, damit sie immer sichtbar ist└─┘— Lückenklammer, die zwischen zwei Balkenenden spannt★/☆— gefüllter/offener Stern, der gleichzeitig als Zeilenbezeichnung des Stern-Histogramms dient- Zeichen für Linienzeichnung (
─│╭╮╰╯) — Verbinder für Liniendiagramme und Trendlinien ●— Streupunktmarker
Keine Farbe, nirgendwo. Das ist das Medium, keine Einschränkung, die umgangen werden muss — ein eingefügter Substack-Codeblock besteht aus wörtlichen Zeichen, sonst bleibt nichts erhalten (keine Inline-CSS, keine gestylten Spans). Jede Unterscheidung, die ein Diagramm treffen muss, steckt im Zeichen/Ziffer/Legende, nicht in einem nicht verfügbaren Farbkanal.
Diagrammtyp-Menü
| Aufgabe | Funktion | Verwende für |
|---|---|---|
| Größenvergleich, mehrere Kategorien, alle beginnen bei 0 | bar(value, max_value, width) + render() | Tanken (5 Min) vs. DC-Schnellladen (45 Min) |
| Ein Min-Max-Bereich, eine Kategorie | range_bar(start, end, scale_max, width, open_ended=) + render() | „$100k–$300k+ installiert“ |
| Zwei Werte, vorher/nachher | Reiner Text: f"{start} → {end}" | „Großhandel $220 → Ausschank $650“ — ein Balken fügt nichts hinzu; der Pfeil ist das Diagramm |
| Ein Maß vs. ein Benchmark-Band + ein Ziel | bullet(value, scale_max, target=, benchmark=) + render() | „Deine Bewertung: 4,2, Normalband 3,8–4,5, Ziel 4,7“ |
| Die Lücke zwischen zwei Balken explizit hervorheben | gap_bracket(a, b, scale_max, width, label=) über render(bracket=) | „Du vs. Marktmedian“ — der charakteristische Schachzug des Berichts |
| Eine Bewertungsverteilung von 1–5 (oder ähnlich) | star_histogram(counts) | Google/Yelp-Sternaufschlüsselung, eine Zeile pro Sternanzahl |
| Zwei kategoriale Achsen, schattiert nach Intensität | heatmap(matrix, row_labels, col_ticks=) | Fußgängerverkehr nach Tag × Stunde — „wann bist du beschäftigt/tot“ |
| Trend über Zeit, eine Reihe | line_chart(values, height, x_labels=) | Monatlicher Umsatz, Anzahl der Bewertungen über ein Jahr |
| Trend, inline in einem Satz | sparkline(values) | „Bewertungen pro Monat: ▂▃▅▆█▇▅▃, 3x höher seit März“ |
| 2 oder 3 benannte Mengen, bei denen der Grad der Überlappung selbst die Erkenntnis ist | venn(labels) | „38% deiner Ein-Stern-Bewertungen erwähnen auch die Wartezeit“ — die Größe der Überlappung ist der Punkt |
| Teile eines Ganzen, eine Momentaufnahme | pie(slices, radius) | Umsatzmix: Kaffee / warme Speisen / Getränke / Lotto |
| Teile eines Ganzen, mit einer Schlagzeilensumme | donut(slices, radius, center_text=) | Gleiches, mit „247 Bewertungen“ in der hohlen Mitte |
| Zwei numerische Variablen, gibt es einen Zusammenhang | scatter(points, width, height) | Preis vs. Bewertung bei Wettbewerbern |
| Gleiches, plus „und hier ist der Trend“ | scatter_with_trend(points, width, height) | Gleiches, mit einer angepassten Linie, die die Korrelation beweist |
Wenn nichts in dieser Tabelle passt, halte inne und denke nach, anstatt eine Form zu erzwingen, die nicht passt — ein Diagramm, das technisch darstellbar, aber verwirrend ist, ist schlimmer, als auf die Chrome-kopflose PNG-Pipeline zurückzugreifen.
Der 2-Sekunden-Test für Diagramme
Ein Diagramm erfüllt denselben Standard wie ein guter Satz: von einem müden Leser auf dem Handy beim ersten Blick verstanden werden, ohne erneutes Lesen, ohne Dekodieren. Wende den Test an, bevor du einen Diagrammtyp auswählst, nicht nachdem du einen erstellt hast.
Der 2-Sekunden-Test: Könnte der beabsichtigte Leser — ein Kleinunternehmer, kein Datenanalyst — innerhalb von 2 Sekunden sagen, was dieses Diagramm bedeutet, ohne zuerst eine Legende zu lesen? Wenn selbst eine Region oder ein Segment das Dekodieren einer Legende erfordert, ist das das visuelle Äquivalent von Fachjargon: technisch präzise, aber es zwingt den Leser zur Arbeit, anstatt ihn mitzuziehen.
Erwischt bei einem echten Problem, als der Herausgeber den gerenderten Beitrag kalt las:
- Das 3-Mengen-Venn-Ziffern-Bitmasken-Diagramm erforderte das Lesen einer 7-zeiligen Legende, bevor eine Region irgendetwas bedeutete. Der Herausgeber: Sie haben es nicht verstanden, also würde ein normaler Geschäftsinhaber es auch nicht. Die zugrundeliegende Tatsache — „du brauchst alle drei, nicht nur eine“ — ist ein Satz, und die nummerierte Liste direkt nach dem Diagramm sagte es bereits in einfachem Englisch. Das Diagramm fügte Dekodierungskosten hinzu, ohne Informationen zu liefern, die der Prosatext nicht schon klarer vermittelte. Korrektur: Entferne es, lass den Satz + die Liste die Arbeit machen.
- Der DC-Schnelllade-Kostenbereichsbalken (ein einzelner Balken, keine Achse, kein Vergleichspunkt) hatte nichts zum Vergleichen. Der Wert eines Balkens ergibt sich aus der Darstellung einer Größe relativ zu etwas — einer Skala, einem anderen Balken, einem Benchmark. Ein einzelner Balken ohne Bezugspunkt ist dekorativ, nicht informativ. Die Zahl, die er veranschaulichte ($100k–$300k+), war bereits einen Satz zuvor klar angegeben. Korrektur: Entferne ihn ebenfalls.
Regeln, die daraus folgen:
- Ein Diagramm, das seine eigene Legende benötigt, um gelesen zu werden, ist ein Warnsignal — nicht nur für Venn. Eine Heatmap ohne Achsenbeschriftung oder eine Torte mit mehr Segmenten, als ein Leser im Kopf behalten kann, versagen auf die gleiche Weise.
- Wenn ein einfacher Satz die Tatsache genauso klar darstellt wie das Diagramm, lasse das Diagramm weg. Gleicher Instinkt wie beim Ersetzen von „mehrere/viele/verschiedene“ durch die tatsächliche Anzahl in einem Text: Greife zu einer Visualisierung, weil ein Vergleich oder Trend der Punkt ist, nicht nur, weil die Daten eine Form haben.
- Jeder Balken braucht eine Achse (
render(..., axis=)) oder einen zweiten Balken auf der gleichen Skala zum Vergleichen. Ein einzelner Balken ohne beides vermittelt nichts — der gleiche Fehler wie ein Passivsatz ohne echtes Subjekt. Aber sobald das erfüllt ist, entferne die Achse, wenn sie nur eine Zahl wiederholen würde, die bereits in einer per-Zeile- Wertbeschriftung steht — ein Gas-gegen-Laden-Balken hat seine0 ... 45 minAchsenlinie aus genau diesem Grund entfernt: Beide Zeilen tragen bereits ihren eigenen Wert („5 Min“, „45 Min“), also wiederholte die Achse nur „45 Min“ ein zweites Mal. Gleiches Prinzip wie das Streichen eines überflüssigen Nebensatzes aus einem Satz. - Erstelle kein Diagramm für eine Zahl, die bereits ein klarer Satz ist. „$100k–$300k+ installiert“ ist als Text maximal klar; es zu diagrammen offenbart keinen Vergleich, Trend oder Überlappung, es fügt nur einen Dekodierungsschritt ohne Nutzen hinzu.
- Lies das fertige Diagramm kalt, so wie der tatsächliche Leser es tun wird — ein Tankstellenbesitzer auf seinem Handy, nicht die Person, die es gerade erstellt hat und bereits weiß, was es sagen soll.
Arbeitsablauf
Verwende scripts/ascii_chart.py, anstatt Zeichen von Hand auszurichten oder
Geometrie selbst zu rastern — Spaltenmathematik und Kreis-/Winkelmathematik sind
genau die Art von Dingen, die man leicht subtil falsch nach Augenmaß machen kann und die sich erst
als Versatz um ein Zeichen oder ein schiefer Kreis im Nachhinein zeigen. Führe python3 scripts/ascii_chart.py
jederzeit aus, um ein Beispiel jedes Diagrammtyps als Plausibilitätsprüfung zu drucken.
Balken / Bereichsbalken
from ascii_chart import bar, range_bar, render
rows = [
("Tanken", bar(5, 45, 24), "5 Min"),
("DC-Schnellladen", bar(45, 45, 24), "45 Min"),
]
print(render(rows, axis=("0", "45 Min")))
row = ("DC-Schnelllader", range_bar(100, 300, 300, 24, open_ended=True), "$100k–$300k+")
print(render([row]))
render() nimmt (label, bar_string, value_str)-Tupel und kümmert sich um die gesamte
Spaltenausrichtung — übergib deinen eigenen formatierten value_str (Währung, Einheiten, was auch
immer der Text benötigt); die Funktion kümmert sich nur um die Geometrie.
Aufzählungspunkt-Diagramm
from ascii_chart import bullet, render
rows = [("Durchschnittsbewertung", bullet(4.2, 5.0, target=4.7, benchmark=(3.8, 4.5), width=24), "4.2 (Ziel 4.7)")]
print(render(rows, axis=("0", "5.0")))
Ein durchgehender Balken für den gemessenen value, gezeichnet über ein optionales schattiertes
benchmark-Band (der „normale“ Bereich, ▒) mit einer optionalen target-Markierung
(┃, immer zuletzt gezeichnet, damit sie über jeder darunter liegenden Füllung/Schattierung sichtbar bleibt). Beantwortet
die Frage „Bin ich im Normalbereich und wie weit bin ich vom Ziel entfernt“ auf einen Blick —
es gibt immer noch nur einen echten Balken (das Maß); das Band ist Kontext.
Lückenklammer
from ascii_chart import bar, gap_bracket, render
rows = [("Du", bar(42000, 60000, 24), "$42,000"), ("Marktmedian", bar(58000, 60000, 24), "$58,000")]
print(render(rows, bracket=gap_bracket(42000, 58000, 60000, 24)))
Eine └─┘-Klammer, die zwischen zwei Balkenenden spannt, mit der Größe der Lücke
in der Mitte (automatisch formatiert aus den beiden Werten, oder übergib label= für deinen
eigenen Text). Verwende den gleichen scale_max/width für die Balken und die
Klammer, sonst läuft die Spanne nicht zusammen. Dies ist „Du vs. der Markt,“
explizit gemacht, anstatt dass der Leser es selbst abschätzen muss.
Stern-Histogramm
from ascii_chart import star_histogram
print(star_histogram([(5, 61), (4, 22), (3, 9), (2, 4), (1, 4)]))
counts ist [(stars, count), ...] in der gewünschten Reihenfolge (normalerweise 5 abwärts bis 1).
Die gefüllten/offenen Sternsymbole (★/☆) dienen gleichzeitig als Zeilenbezeichnung —
es ist nicht nötig, auch noch „5 Sterne“ im Text auszuschreiben. Vollständig auf
bar() + render() aufgebaut.
Heatmap
from ascii_chart import heatmap
print(heatmap(matrix, ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
col_ticks=[(0, "24:00"), (6, "06:00"), (12, "12:00"), (18, "18:00")]))
matrix ist Zeilen × Spalten von numerischer Intensität (z. B. 7 Tage × 24 Stunden),
normalisiert über die gesamte Matrix (nicht pro Zeile), damit die Schattierung über die Zeilen hinweg
vergleichbar ist. col_ticks ist [(column_index, label), ...] — kein
automatischer Abstand, wähle Positionen, die für die übergebenen Beschriftungen nicht kollidieren.
Liniendiagramm / Sparkline
from ascii_chart import line_chart, sparkline
print(line_chart([120, 135, 128, 150, 175, 190, 205, 198, 220],
height=10, x_labels=("Jan", "Sep")))
print(sparkline([3, 5, 4, 8, 12, 10, 15, 14, 18]))
line_chart zeichnet eine verbundene Linie mit Linienzeichnungsecken (╭╮╰╯│─) —
es gibt kein Zeichen für eine beliebige dazwischenliegende Steigung, daher ist die Breite fest
bei len(values) - 1 Spalten (eine pro Segment). x_labels, falls angegeben, ist
nur (first, last), gedruckt auf der Achsenzeile — sage die Zwischenpunkte im
umgebenden Text, versuche nicht, jede Markierung zu beschriften. sparkline ist ein
einzeiliger 8-stufiger Trend (▁▂▃▄▅▆▇█) für einen Satz innerhalb eines Satzes, kein
eigenständiges Diagramm.
Venn (2 oder 3 Mengen)
from ascii_chart import venn
diagram, legend = venn(["Autobahnausfahrt", "Großes Geschäft", "Netzkapazität"])
print(diagram)
print(legend)
Rasterisiert überlappende Kreise und füllt jede Region mit einer Ziffer
(eine Bitmaske, zu welcher(n) Menge(n) sie gehört: 1, 2, 4 für die einfachen Mengen,
3/5/6 für paarweise Überlappungen, 7 für alle drei), anstatt zu versuchen,
Beschriftungstext in eine gekrümmte Grenze einzupassen, was in der Monospace-Darstellung
nicht funktioniert. Die Legende (separat zurückgegeben, beide ausgeben) ordnet jede Ziffer
wieder der tatsächlichen Beschriftungskombination zu, z. B. 7 = Autobahnausfahrt + Großes Geschäft + Netzkapazität < - alle. Dies wurde ursprünglich für unmöglich in ASCII gehalten
(ein früher Beitrag griff auf ein kopfloses Chrome-PNG für sein Venn zurück) — die
Ziffern-Region-Technik löst es sauber; es gibt keinen Fall mehr, in dem Venn auf den
PNG-Fallback angewiesen ist.
Aber führe es vorher durch den 2-Sekunden-Test oben — die Ziffernlegende selbst ist das Ding, das ein echtes Venn für einen Laienleser verwirrend gemacht hat. Ein Venn verdient seinen Platz, wenn der Grad der Überlappung die Erkenntnis ist (z. B. „38% deiner Ein-Stern-Bewertungen erwähnen auch die Wartezeit“). Wenn die wahre Botschaft „du brauchst alle drei zusammen, keine Teilpunkte“ ist, ist ein Venn das falsche Werkzeug, selbst wenn es korrekt rendert — reiner Text erledigt diese Arbeit ohne Legende, z. B.:
Autobahnausfahrt + Großes Geschäft + Netzkapazität
Verpasse eines davon, überspringe den Standort
Torte / Ring
from ascii_chart import pie, donut
diagram, legend = pie([("Kaffee", 40), ("Warme Speisen", 25), ("Getränke", 20), ("Lotto", 15)], radius=9)
print(diagram); print(legend)
diagram, legend = donut([("Kaffee", 40), ("Warme Speisen", 25), ("Getränke", 20), ("Lotto", 15)],
radius=9, center_text="247")
print(diagram); print(legend)
Segmente beginnen bei 12 Uhr und verlaufen im Uhrzeigersinn (Standard-Tortenkonvention), jedes
bekommt ein eigenes Füllzeichen (█▓▒░▚▞▤▥, niemals auf einem benachbarten Segment wiederholt bei
üblichen 2–6 Segmentanzahlen), sodass die Grenzen ohne Farbe lesbar bleiben. donut ist pie mit einer hohlen Mitte — center_text setzt eine
Schlagzeilenzahl/-bezeichnung dorthin (z. B. eine Gesamtanzahl), was das eine Ding ist, das ein
ASCII-Ring tun kann, das ein echter normalerweise mit leerem Raum verschwendet. radius ist in Zeileneinheiten; die Funktion korrigiert intern das
Seitenverhältnis von Monospace-Zeichen (gemeinsame ASPECT-Konstante, auch verwendet von venn()),
sodass das Ergebnis tatsächlich rund ist, kein abgeflachtes Oval — stimme Breite/Höhe nicht
von Hand ab, übergib einfach radius.
Korrektur: ASPECT wurde auf angenommene 2.0 hartcodiert und war nie
richtig getestet worden. Tatsächlich ist es ~2,49 — das sind nicht 2,0. Wenn du eine
Torte oder einen Ring von Hand gebaut hast und sie schief aussahen, lag es an diesem Wert.
Die Korrektur ist jetzt eingebaut; du musst nichts ändern, aber wenn du die Ausgabe
vor und nach der Korrektur vergleichst, sind runde Ergebnisse der neue Standard.
Allgemeine Technik
Größenanpassung — maximale Breite
Diagramme sind auf 44 Spalten begrenzt (maximale Zeilenbreite, die
auf einem iPhone Safari-Viewport mit 390px Breite vollständig sichtbar ist, gemessen im Februar
2025). Etwas darüber hinausgehende Balken/Streudiagramme laufen Gefahr, am rechten Rand abgeschnitten zu werden, ohne
jegliches Scrollen. Wenn die eingegebenen nativen Werte breiter sind, passe die
width/radius-Argumente an, um darunter zu bleiben, oder erwäge die Aufteilung in
mehrere Diagramme (was normalerweise besser erzählt).
Die Begrenzung liegt im Codebaustein, der das Diagramm enthält, nicht im Diagramm selbst. Ein guter Daumenwert ist 44 Spalten; Newsletter-Plattformen mit breiteren Codeblöcken (z. B. beeindruckende 50+ Spalten in einer Desktop-Mail-App) werden sie trotzdem vollständig zeigen. Aber die mobile Ansicht des Newsletters ist der Kanal, der die Obergrenze setzt.
Abstände und Ausrichtung (render())
Balkenarten (Balken, Bereich, Aufzählungspunkt, Lückenklammer) folgen dem gleichen Layout:
[label-Polsterung] [Balken] [Wert]
render()berechnet die maximale Länge aller Label + Balken + Wert Zeilen und polstert die Label-Spalte auf gleiche Breite auflabelwird linksbündig ausgerichtet,value_strwird an Ort und Stelle gelassen, Balken füllt die Mitte- Übergib denselben
scale_maxan jeden Balken in einem Aufruf vonrender(), damit alle auf der gleichen Skala sind
Die +-Konvention (offener Bereich)
Wenn ein Wert außerhalb des sichtbaren Maximalwerts liegt (eine Kostenschätzung, die höher läuft, als
du auf dem Bildschirm platzieren kannst), setze open_ended=True, um ein + an das Ende des Balkens
anzuhängen, anstatt den Balken selbst zu dehnen, um eine Zahl vorzutäuschen, die du nicht hast. Der Leser sieht
einen Balken, der fast die volle Breite erreicht, plus ein +, das sagt: „und dann noch mehr“.
GUID: 2016judea/ascii-charts
Repository: https://github.com/2016judea/ascii-charts
Alles testen
Das Skript gibt einen Beispiel-Block jedes Diagrammtyps aus. Führe es aus, um eine einfache visuelle Überprüfung zu erhalten:
python3 scripts/ascii_chart.py