ASCII チャート
プレーンなUnicodeブロック文字で構築され、リテラルテキストとして埋め込まれるターミナル風チャート:Markdown内のフェンスドコードブロック、HTML内の<pre>タグ。チャートサービスなし、「made with」フッターなし、外部アカウント不要、そしてモノスペースがレンダリングされる場所ならどこでも同じように表示されます。
scripts/ascii_chart.py にはチャートタイプごとに1つの関数があります。python3 scripts/ascii_chart.py を実行すると、すべてのタイプの例が1つずつ出力され、正常性確認ができます。
ここでは2つのことが測定(当初は誤っていました)されています:390pxの電話ビューポートでは、約44文字を超える行は右端がクリップされ、モノスペースで「丸い」円を描くには約2.49(2.0ではない)の水平アスペクト補正が必要です。「サイジング」と「円グラフ/ドーナツ」のセクションを参照してください。
ニュースレタープラットフォームによって、コードブロックの貼り付け方は異なります。実際に依存する前に、テストチャートを実際のドラフトに貼り付け、Web、アプリ、メールでのレンダリングを確認してください。
文字セット
█▓▒░(U+2588, U+2593, U+2592, U+2591 — 全/濃/中/淡シェード) — 塗りつぶし密度。「最も多い」から「最も少ない」まで。棒グラフは█/░のみ(塗りつぶしvs.トラック)を使用;円グラフ/ドーナツは4つすべて(+スライスが4つを超える場合は▚/▞/▤/▥)をスライスごとの個別の塗りつぶしとして使用;ヒートマップは4つすべてに加えて最低レベルに·(中央ドット、空白ではない)を使用するため、「静かな」セルも意図的なグリッドの一部として読み取られ、ギャップではありません。- 数字
1–7— ベン図領域コード(領域が属するセットのビットマスク — 以下のベン図を参照) +— 表示されている上限よりも高くなる可能性があるレンジバー上のオープンエンドフラグ(持っていない数字を偽造するためにバー自体を伸ばさない)┃— バレットチャートのターゲットティック。下にある塗りつぶし/シェーディングの上に描画されるため、常に表示されます。└─┘— ギャップブラケット。2つのバーの端の間にまたがります。★/☆— 塗りつぶし/白抜きの星。スター・ヒストグラムの行ラベルを兼ねます。- 罫線 (
─│╭╮╰╯) — 折れ線グラフとトレンド線のコネクタ。 ●— 散布図の点マーカー。
どこにも色はありません。それは媒体であり、回避すべき制限ではありません — 貼り付けられたSubstackのコードブロックはリテラル文字であり、それ以外は何も残りません(インラインCSS、スタイル付きスパンは不可)。チャートが行う必要のある区別はすべて、利用できない色チャネルではなく、文字/数字/凡例に生きています。
チャートタイプメニュー
| 用途 | 関数 | 使用例 |
|---|---|---|
| すべて0から始まる、複数のカテゴリの大きさ比較 | bar(value, max_value, width) + render() | ガス充填(5分)vs. DC急速充電(45分) |
| 1カテゴリの最小値–最大値の範囲 | range_bar(start, end, scale_max, width, open_ended=) + render() | 「$100k–$300k+ 導入済み」 |
| 2つの値、前/後 | プレーンテキスト:f"{start} → {end}" | 「卸売 $220 → タップルーム $650」 — 棒グラフは何も追加しません;矢印自体がチャートです |
| ベンチマーク帯とターゲットに対する1つの測定値 | bullet(value, scale_max, target=, benchmark=) + render() | 「あなたの評価:4.2、通常帯域 3.8–4.5、ターゲット 4.7」 |
| 2つのバー間のギャップを明示的に指摘 | gap_bracket(a, b, scale_max, width, label=) を render(bracket=) 経由で使用 | 「あなた vs. 市場中央値」 — レポートの特徴的な動き |
| 1–5(または同様の)評価分布 | star_histogram(counts) | Google/Yelp スター内訳、星の数ごとに1行 |
| 2つのカテゴリ軸、強度でシェーディング | heatmap(matrix, row_labels, col_ticks=) | 曜日×時間のフットトラフィック — 「いつ忙しい/暇か」 |
| 1系列の経時トレンド | line_chart(values, height, x_labels=) | 月間収益、1年間のレビュー数 |
| 文中のインライントレンド | sparkline(values) | 「月間レビュー数:▂▃▅▆█▇▅▃、3月から3倍増加」 |
| 2つまたは3つの名前付きセットで、重なりの程度自体が発見事項 | venn(labels) | 「あなたの1つ星レビューの38%が待ち時間にも言及しています」 — 重なりの大きさがポイント |
| 全体の一部、1つのスナップショット | pie(slices, radius) | 収益構成:コーヒー / 温かい料理 / 飲み物 / 宝くじ |
| 全体の一部、見出しの合計付き | donut(slices, radius, center_text=) | 同上、「247件のレビュー」が中空の中心に表示される |
| 2つの数値変数、関係性はあるか | scatter(points, width, height) | 競合他社間の価格 vs. 評価 |
| 同上、さらに「そしてこれがトレンド」 | scatter_with_trend(points, width, height) | 同上、相関を証明する近似線付き |
この表に当てはまるものが何もない場合は、適切でない形状を無理に使うのではなく、立ち止まって考えてください — 技術的にはレンダリング可能だが混乱を招くチャートは、ChromeヘッドレスPNGパイプラインに戻るよりも悪いです。
チャートの2秒テスト
チャートは良い文章と同じ基準に答える必要があります:疲れた読者が電話で、一目で、読み返さず、解読せずに理解できること。チャートタイプを選ぶ前にこのテストを適用し、構築した後ではなく適用してください。
2秒テスト: 意図された読者(データアナリストではなく、中小企業の経営者)が、最初に凡例を読むことなく、2秒以内にこのチャートが何を意味するか言えるでしょうか? 1つの領域やスライスを理解するためにまず凡例を解読する必要がある場合、それは専門用語の視覚的同等物です:技術的に正確ですが、読者に作業を強いることになり、読者を引き込むのではなく遠ざけます。
実際の課題で発見されました (編集者が公開された投稿を冷ややかに読んで):
- 3セットのベン図デジットビットマスク図は、どの領域も意味を持つ前に7行の凡例を読む必要がありました。編集者:彼らは理解できなかったので、一般の事業主も理解できないでしょう。根底にある事実「これら3つすべてが必要であり、1つだけではない」は一文であり、図の直後の番号付きリストですでに平易な英語で述べられていました。図は、散文がすでに明確に伝えている情報に加えて何も提供せず、解読コストを追加しました。修正:削除し、文+リストに任せる。
- DC急速充電器のコストレンジバー(単一のバー、軸なし、比較ポイントなし)には比較対象がありませんでした。バーの価値は、何かに対する大きさを示すことから生まれます — スケール、別のバー、ベンチマーク。基準点のない単独のバーは装飾的であり、情報的ではありません。それが示していた数字($100k–$300k+)は、1文前にすでに明確に述べられていました。修正:これも削除。
結果としてのルール:
- 読むために独自の凡例を必要とするチャートは警告サインです — ベン図だけではありません。ラベルのない軸を持つヒートマップ、または読者が頭の中で保持できる数を超えるスライスを持つ円グラフも、同じように失敗します。
- 平易な文がチャートと同じくらい明確に事実を述べている場合は、チャートを省略します。これは、散文で「いくつかの/多くの/様々な」を実際の数に置き換えるのと同じ感覚です:比較やトレンドがポイントだから視覚的表現に頼るのであって、データに形状があるからだけではありません。
- すべてのバーには軸(
render(..., axis=))か、同じスケールで比較するための2番目のバーが必要です。どちらもない単一のバーは何も伝えません — これは、実質的な主語のない受動態の文と同じ失敗です。しかし、それが満たされたら、各行の値ラベルにすでにある数字を繰り返すだけの場合は軸を削除します — あるガスvs.充電のバーは、まさにこの理由で「0 ... 45 min」の軸線を削除しました:両方の行がすでに独自の値(「5 min」、「45 min」)を持っているため、軸は「45 min」を2回目に繰り返すだけでした。文から冗長な節を削除するのと同じ原理です。 - すでに1つのきれいな文になっている数字はチャート化しないでください。「$100k–$300k+ 導入済み」はテキストとして最大限明確です;それをチャート化しても比較、トレンド、重なりは明らかにならず、見返りのない解読ステップを追加するだけです。
- 完成したチャートを、実際の読者が読むように冷ややかに読んでください — ガソリンスタンドの経営者が電話で、ちょうどそれを構築して何を言うべきかすでに知っている人ではありません。
ワークフロー
自分で文字を手動で整列させたりジオメトリをラスタライズしたりするのではなく、scripts/ascii_chart.py を使用してください — 列の計算と円/角度の計算は、目視で微妙に間違えやすく、後になって1文字の位置ずれや歪んだ円としてしか現れない種類のものです。python3 scripts/ascii_chart.py をいつでも実行すると、すべてのチャートタイプの例が1つずつ出力され、正常性確認ができます。
棒グラフ / レンジバー
from ascii_chart import bar, range_bar, render
rows = [
("ガス充填", bar(5, 45, 24), "5 min"),
("DC急速充電", bar(45, 45, 24), "45 min"),
]
print(render(rows, axis=("0", "45 min")))
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 ティック(┃、下の塗りつぶし/シェーディングの上に常に表示されるように最後に描画)が付きます。「私は正常範囲内か、ターゲットからどれくらい離れているか」を一目で答えます — 実際のバーは1つだけ(測定値)であり、帯域はコンテキストです。
ギャップブラケット
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)))
2つのバーの端の間にまたがる └─┘ ブラケットで、ギャップのサイズがその中央に表示されます(2つの値から自動フォーマットされるか、独自のテキストのために 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, "午前0時"), (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=("1月", "9月")))
print(sparkline([3, 5, 4, 8, 12, 10, 15, 14, 18]))
line_chart は罫線コーナー(╭╮╰╯│─)で接続線を描画します — 任意の中間勾配の文字はないため、幅は len(values) - 1 列(セグメントごとに1つ)に固定されます。x_labels が指定された場合、それは単に (first, last) で、軸行に印刷されます — 中間の点については周囲の散文で説明し、すべてのティックにラベルを付けようとしないでください。sparkline は、単一行の8レベルトレンド(▁▂▃▄▅▆▇█)で、文の中の1節のためにあり、スタンドアローンのチャートではありません。
ベン図(2つまたは3つのセット)
from ascii_chart import venn
diagram, legend = venn(["高速道路出口", "大型店舗", "送電容量"])
print(diagram)
print(legend)
重なり合う円をラスタライズし、各領域を数字(それが属するセットのビットマスク:単一セットは 1、2、4、ペアワイズ重なりは 3/5/6、3つすべては 7)で塗りつぶします。これは、曲線境界内にラベルテキストを収めようとする代わりであり、モノスペースレンダリングでは機能しません。凡例(別々に返されるので、両方を印刷します)は、各数字を実際のラベルの組み合わせにマッピングします。例:7 = 高速道路出口 + 大型店舗 + 送電容量 <- すべて。これは当初ASCIIでは不可能だと考えられていました(初期の投稿ではベン図にヘッドレスChrome PNGにフォールバックしていました) — 数字領域テクニックがこれをきれいに解決します;ベン図がPNGフォールバックを必要とするケースはもはやありません。
しかし、それを使う前に上記の2秒テストを実行してください — 数字の凡例自体が、実際のベン図を一般の読者にとって混乱させたものです。 ベン図は、重なりの程度が発見事項である場合にその価値を発揮します(例:「あなたの1つ星レビューの38%が待ち時間にも言及しています」)。実際のメッセージが「これら3つすべてが一緒に必要で、部分的なクレジットはありません」である場合、ベン図は正しくレンダリングされても間違ったツールです — プレーンテキストが凡例なしでその仕事をします。例:
高速道路出口 + 大型店舗 + 送電容量
これらを1つでも欠くと、サイトをスキップ
円グラフ / ドーナツ
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ドーナツが実際のドーナツが通常空きスペースで無駄にすることを1つだけ実行できることです。radius は行単位です;関数はモノスペース文字のアスペクト比(共有の ASPECT 定数、venn() でも使用)を内部で補正するため、結果は実際に円形になり、平らな楕円にはなりません — 幅/高さを個別に手動調整せず、単に radius を渡してください。
補正: ASPECT は想定された 2.0 にハードコードされており、決して検証されていませんでした。2.49 がモノスペースの実際の補正であり、結果として得られる円が視覚的に正しい唯一のものです。以前のすべての出力は垂直方向にわずかに圧縮された楕円でした。サイズは適切に見えていたため、誰も気づきませんでした。
散布図
from ascii_chart import scatter, scatter_with_trend
print(scatter([(2, 3), (5, 8), (7, 12), (10, 9), (12, 15), (15, 14), (18, 18), (20, 20)],
width=20, height=10))
print(scatter_with_trend([(2, 3), (5, 8), (7, 12), (10, 9), (12, 15), (15, 14), (18, 18), (20, 20)],
width=20, height=10))
points は (x, y) タプルのリストで、width と height は描画キャンバスの文字単位の寸法です(データ座標ではありません — 関数はすべての点が収まるように正規化します。scatter_with_trend は、正規化されたデータに最小二乗線を追加します。
両関数とも、軸ラベル、グリッド線、または凡例は追加しません — これらは散らかりを追加し、読者がそれらを解読しようとすることで2秒テストに失敗します。代わりに、軸の範囲を周囲の散文で説明し、データが何を示しているかを明確にします。
FAQ
これらのチャートをどこに貼り付けられますか?
これらはリテラルテキストです — Markdownのコードブロック(Gist、GitHub、Substack、Ghost、Notion)、<pre> HTMLタグ、ターミナル出力、チャットアプリ、プレーンテキストメール。モノスペースフォントでレンダリングされる任意の場所。
特定のプラットフォーム向けにテストする必要がありますか?
はい。特にニュースレター(Substack、Buttondown、ConvertKitなど)は、特に長い行を異なる方法で処理する場合があります。実際の下書きにテスト出力を貼り付け、Webとモバイルの両方のプレビューで確認してください。
このツールは私のデータをどこかに送信しますか?
いいえ。すべてはローカルで実行され、依存関係はなく、ネットワークコールもありません。スクリプトは保護された顧客データを決して扱いません。
色が必要な場合はどうすればいいですか?
色は達成しようとしている媒体ではありません。これらのチャートは、貼り付けられたテキストとして生き残る文字に依存しています。色付きの出力が必要な場合は、別のツール(Chromeヘッドレス、Matplotlibなど)が必要になります。
より多くの点を持つ散布図用の文字はありますか?
現在の実装は単一のマーカー ● です。より多くの点がある場合(例:50+)、重なり合う点は依然として1つとしてレンダリングされます。エリアが高密度で色なしの点の塊になるため、これはデータ密度が高すぎることを示しています。その場合は、上記の2秒テストを再検討し、テキストでデータを要約することを検討してください。
なぜASPECTは2.0ではなく2.49なのですか?
2.0は、モノスペースフォントでの実際の表示アスペクト比についての想定でした。最も狭いテスト(半径の異なる2つの描画円を視覚的に比較する)は、2.49 が視覚的に正しい唯一の値であることを示しています。2.0 を使用した以前のすべての出力は、垂直方向にわずかに圧縮された楕円でした。以前の出力はサイズ的には適切に見えていたため、このエラーは何ヶ月も気づかれませんでした — 歪みは微妙で、出力を他のものと比較しない限り目立ちませんでした。
このコードを自分で変更できますか?
はい。スクリプトは単一のファイル scripts/ascii_chart.py で、依存関係はなく、変更が容易です。サポートされているチャートタイプのいずれかの上に新しいチャートタイプを追加したり、文字セットを変更したりできます。