ASCII 圖表
使用純 Unicode 區塊字符構建的終端機風格圖表,可嵌入為純文字:Markdown 中的圍欄式程式碼區塊、HTML 中的 <pre> 標籤。無需圖表服務、無「由…製作」註腳、無外部帳戶,且在等寬渲染的任何地方呈現一致。
scripts/ascii_chart.py 對每種圖表類型有一個函數。執行 python3 scripts/ascii_chart.py 可列印每種類型的範例作為完整性檢查。
這裡有兩件事是經過測量而非假設的,而且最初都是錯誤的:在 390px 的手機視口中,超過 ~44 個字符的行會從右邊緣被截斷;而等寬字體中的「圓形」需要大約 2.49 的水平長寬比校正(而非 2.0)。請參閱「尺寸」和「圓餅圖/環圈圖」小節。
電子報平台在貼上程式碼區塊的方式上有所不同。在依賴某個平台之前,請先將測試圖表貼入實際草稿,並檢查網頁、應用程式和電子郵件的呈現效果。
字符集
█▓▒░(U+2588, U+2593, U+2592, U+2591 — 全色/深色/中色/淺色陰影) — 填充密度,從「最多」到「最少」。長條圖僅使用█/░(填充 vs. 軌道);圓餅圖/環圈圖使用全部四種(如果切片超過 4 個,還會使用▚/▞/▤/▥作為不同的逐切片填充);熱力圖使用全部四種加上·(中間點,非空白空間)作為最低層級,因此「安靜」的儲存格仍然可被視為網格的有意義部分,而非間隙。- 數字
1–7— 文氏圖區域代碼(該區域所屬集合的位元遮罩 — 請參閱下方的文氏圖) +— 範圍條上的開放式旗標,表示可能超過顯示的上限(不要拉伸條本身來偽造您沒有的數字)┃— 子彈圖的目標刻度,繪製在任何填充/陰影之上,以便始終可見└─┘— 缺口支架,跨度在兩個條的末端之間★/☆— 實心/空心星號,同時作為星號直方圖的行標籤- 框線繪製 (
─│╭╮╰╯) — 折線圖和趨勢線的連接器 ●— 散佈圖點標記
任何地方都不使用顏色。這就是媒介,而非需要繞過的限制——貼上的 Substack 程式碼區塊就是純文字字符,其他任何東西都無法保留(無 inline CSS、無樣式化的 span)。圖表需要進行的任何區分都存在於字符/數字/圖例中,而非不可用的顏色通道。
圖表類型選單
| 任務 | 函數 | 用途 |
|---|---|---|
| 數值比較,多個類別,全部從 0 開始 | bar(value, max_value, width) + render() | 加油(5 分鐘)與 DC 快充(45 分鐘) |
| 最小-最大範圍,單一類別 | range_bar(start, end, scale_max, width, open_ended=) + render() | 「$100k–$300k+ 已安裝」 |
| 兩個值,之前/之後 | 純文字:f"{start} → {end}" | 「批發 $220 → 品飲室 $650」——長條圖不會增加任何東西;箭頭就是圖表 |
| 單一度量 vs. 基準帶 + 目標 | bullet(value, scale_max, target=, benchmark=) + render() | 「您的評分:4.2,正常區間 3.8–4.5,目標 4.7」 |
| 明確指出兩個條之間的差距 | gap_bracket(a, b, scale_max, width, label=) 透過 render(bracket=) | 「您 vs. 市場中位數」——報告的招牌動作 |
| 1–5 星(或類似)評分分佈 | star_histogram(counts) | Google/Yelp 星級細分,每個星數一行 |
| 兩個分類軸,按強度著色 | heatmap(matrix, row_labels, col_ticks=) | 按日 × 小時的人流量——「您何時忙碌/冷清」 |
| 隨時間變化的趨勢,單一系列 | line_chart(values, height, x_labels=) | 月營收、一年內的評論數量 |
| 趨勢,內嵌在句子中 | sparkline(values) | 「每月評論數:▂▃▅▆█▇▅▃,自三月起成長 3 倍」 |
| 2 或 3 個命名集合,其中重疊程度本身就是發現 | venn(labels) | 「您 38% 的一星評論也提到了等待時間」——重疊大小是重點 |
| 整體的組成部分,單一快照 | pie(slices, radius) | 營收組合:咖啡 / 熱食 / 飲料 / 樂透 |
| 整體的組成部分,附帶總數標題 | donut(slices, radius, center_text=) | 同上,但中心空心處顯示「247 則評論」 |
| 兩個數值變數,是否存在關係 | scatter(points, width, height) | 競爭對手的價格 vs. 評分 |
| 同上,加上「這是趨勢」 | scatter_with_trend(points, width, height) | 同上,加上擬合線證明相關性 |
如果此表中沒有任何內容符合,請停下來仔細思考,而不是強行套用不合適的形狀——技術上可渲染但令人困惑的圖表,比回退到 Chrome 無頭 PNG 管道更糟糕。
圖表的 2 秒測試
圖表與好句子的標準相同:讓疲憊的讀者在手機上,第一眼就能理解,無需重讀、無需解碼。在選擇圖表類型之前應用此測試,而不是在構建之後。
2 秒測試: 目標讀者——一位小型企業老闆,而非數據分析師——能否在 2 秒內說出此圖表的含義,而無需先閱讀圖例?如果理解某個區域或切片需要先解碼圖例,那麼這在視覺上等同於行話:技術上精確,但會讓讀者付出努力,而不是引導他們前進。
在真實問題中發現, 由編輯在閱讀已發佈的貼文時發現:
- 3 集合文氏圖的數字位元遮罩圖需要在理解任何區域之前先閱讀一個 7 行的圖例。編輯:他們不理解,所以一般企業主也不會理解。底層事實——「您需要所有這三項,而不僅僅是一項」——就是一句話,而且圖表後面的編號列表已經用簡單的英文說明了。圖表增加了解碼成本,卻沒有增加散文未更清晰表達的資訊。修正:刪除它,讓句子 + 列表完成工作。
- DC 快充樁成本範圍條(單一條,無軸,無比較點)沒有可比較的對象。長條圖的價值來自於顯示相對於某事物的大小——一個刻度、另一個條、一個基準。一個沒有參考點的單一條是裝飾性的,而非資訊性的。它所說明的數字($100k–$300k+)已經在之前一句話中清楚陳述了。修正:也刪除它。
遵循的規則:
- 需要自己的圖例才能閱讀的圖表是一個警告信號——不僅僅對於文氏圖。具有未標記軸的熱力圖,或切片數量超出讀者記憶能力的圓餅圖,也會以同樣的方式失敗。
- 如果一個簡單句子能像圖表一樣清晰地說明事實,那就跳過圖表。與在散文中用實際計數替換「一些/許多/各種」的本能相同:因為比較或趨勢是重點,而不僅僅是數據具有形狀。
- 每個長條圖都需要一個軸(
render(..., axis=))或同一刻度上的第二個長條圖來進行比較。一個既沒有軸也沒有比較對象的單一條什麼也無法傳達——與沒有真正主語的被動語態句子相同的失敗。但一旦滿足了這個條件,如果軸只會重複已經存在於每行數值標籤中的數字,就省略它——一個汽油與充電的長條圖正好出於這個原因刪除了它的0 ... 45 min軸線:兩行都已經帶有它們自己的數值(「5 分鐘」、「45 分鐘」),所以軸線只是第二次重複了「45 分鐘」。與刪除句子中冗餘從句的原則相同。 - 不要圖表化已經是一個清晰句子的數字。「$100k–$300k+ 已安裝」作為文字已經非常清晰;對其進行圖表化不會揭示比較、趨勢或重疊,只會無謂地增加解碼步驟。
- 像實際讀者那樣冷讀完成的圖表——在手機上的加油站老闆,而不是剛剛構建它並且已經知道它應該說什麼的人。
工作流程
使用 scripts/ascii_chart.py,而不是手動對齊字符或自行光柵化幾何圖形——列數學和圓形/角度數學正是那種容易透過目測微妙出錯,並且只有在事後才會表現為一個字符的錯位或不圓的圓圈的問題。隨時執行 python3 scripts/ascii_chart.py 以列印每種圖表類型的一個範例作為完整性檢查。
長條圖 / 範圍條
from ascii_chart import bar, range_bar, render
rows = [
("加油", bar(5, 45, 24), "5 分鐘"),
("DC 快充", bar(45, 45, 24), "45 分鐘"),
]
print(render(rows, axis=("0", "45 分鐘")))
row = ("DC 快充樁", range_bar(100, 300, 300, 24, open_ended=True), "$100k–$300k+")
print(render([row]))
render() 接受 (label, bar_string, value_str) 元組並處理所有列對齊——傳遞您自己預先格式化的 value_str(貨幣、單位等,取決於文案需要);該函數僅處理幾何圖形。
子彈圖
from ascii_chart import bullet, render
rows = [("平均評分", bullet(4.2, 5.0, target=4.7, benchmark=(3.8, 4.5), width=24), "4.2 (目標 4.7)")]
print(render(rows, axis=("0", "5.0")))
一個用於測量 value 的實心條,繪製在可選的陰影 benchmark 帶(「正常」範圍,▒)之上,帶有可選的 target 刻度(┃,始終最後繪製,以便在底部的任何填充/陰影上保持可見)。一眼就能回答「我是否在正常範圍內,距離目標多遠」——仍然只有一個真正的條(度量);帶只是背景資訊。
缺口支架
from ascii_chart import bar, gap_bracket, render
rows = [("您", bar(42000, 60000, 24), "$42,000"), ("市場中位數", bar(58000, 60000, 24), "$58,000")]
print(render(rows, bracket=gap_bracket(42000, 58000, 60000, 24)))
一個 └─┘ 支架跨越兩個條的末端之間,間隙的大小居中顯示在支架內(從兩個值自動格式化,或傳遞 label= 以使用您自己的文字)。對條和支架使用相同的 scale_max/width,否則跨度將無法對齊。這是「您 vs. 市場」,明確顯示,而不是留給讀者目測。
星號直方圖
from ascii_chart import star_histogram
print(star_histogram([(5, 61), (4, 22), (3, 9), (2, 4), (1, 4)]))
counts 是 [(stars, count), ...],按您想要列印的順序排列(通常是 5 到 1)。實心/空心星號字形(★/☆)同時充當行標籤——無需再在文字中拼出「5 星」。完全基於 bar() + render() 構建。
熱力圖
from ascii_chart import heatmap
print(heatmap(matrix, ["週一", "週二", "週三", "週四", "週五", "週六", "週日"],
col_ticks=[(0, "上午 12 點"), (6, "上午 6 點"), (12, "中午 12 點"), (18, "下午 6 點")]))
matrix 是行 × 列的數值強度(例如 7 天 × 24 小時),在整個矩陣(非逐行)上進行歸一化,以便各行的陰影可進行比較。col_ticks 是 [(column_index, label), ...]——無自動間距,為您傳遞的標籤選擇不會衝突的位置。
折線圖 / 火花線
from ascii_chart import line_chart, sparkline
print(line_chart([120, 135, 128, 150, 175, 190, 205, 198, 220],
height=10, x_labels=("一月", "九月")))
print(sparkline([3, 5, 4, 8, 12, 10, 15, 14, 18]))
line_chart 使用框線繪製的角落(╭╮╰╯│─)繪製一條連接線——沒有字符表示任意中間斜率,因此寬度固定為 len(values) - 1 列(每段一列)。x_labels(如果給定)只是在軸行上列印 (first, last)——在周圍的散文中說明中間點,不要嘗試標記每個刻度。sparkline 是一個單行的 8 層級趨勢(▁▂▃▄▅▆▇█),用於句子中的一個子句,而不是一個獨立的圖表。
文氏圖(2 或 3 集合)
from ascii_chart import venn
diagram, legend = venn(["高速公路出口", "大商店", "電網容量"])
print(diagram)
print(legend)
將重疊的圓形光柵化,並用數字(它所屬集合的位元遮罩:1、2、4 用於純集合,3/5/6 用於成對重疊,7 用於所有三個)填充每個區域,而不是試圖將標籤文字塞入彎曲的邊界內,後者在等寬渲染中無法存活。圖例(單獨返回,列印兩者)將每個數字映射回真實的標籤組合,例如 7 = 高速公路出口 + 大商店 + 電網容量 <- 全部三者。這最初被認為在 ASCII 中是不可能的(早期的貼文退回到使用無頭 Chrome PNG 來呈現文氏圖)——數字區域技術乾淨地解決了這個問題;不再有文氏圖需要 PNG 回退的情況。
但在使用之前,請透過上述的 2 秒測試進行檢查——數字圖例本身正是使一個真實的文氏圖對普通讀者造成困惑的原因。 當重疊程度是發現時(例如「您 38% 的一星評論也提到了等待時間」),文氏圖才有其存在價值。當真正的訊息是「您需要全部三者一起,沒有部分積分」時,即使文氏圖正確渲染,它也是錯誤的工具——純文字可以在無需圖例的情況下完成這項工作,例如:
高速公路出口 + 大商店 + 電網容量
錯過其中一個,跳過這個站點
圓餅圖 / 環圈圖
from ascii_chart import pie, donut
diagram, legend = pie([("咖啡", 40), ("熱食", 25), ("飲料", 20), ("樂透", 15)], radius=9)
print(diagram); print(legend)
diagram, legend = donut([("咖啡", 40), ("熱食", 25), ("飲料", 20), ("樂透", 15)],
radius=9, center_text="247")
print(diagram); print(legend)
切片從鐘面 12 點方向開始,順時針方向(標準圓餅圖慣例),每個切片獲得一個不同的填充字符(█▓▒░▚▞▤▥,對於典型的 2–6 個切片計數,從不在相鄰切片上重複),以便邊界在沒有顏色的情況下保持可讀。donut 是帶有空心中心的 pie——center_text 將一個標題數字/標籤放在那裡(例如總數),這是 ASCII 環圈圖可以做到而真實環圈圖通常浪費在空白空間上的一件事。radius 以行為單位;該函數在內部校正等寬字符的長寬比(共享的 ASPECT 常數,venn() 也使用它),因此結果實際上是圓形的,而不是扁平的橢圓——不要分別手動調整寬度/高度,只需傳遞 radius。
修正: ASPECT 之前被硬編碼為假設的 2.0,並且從未……
…(翻譯截斷)