ASCII 차트
유니코드 블록 문자로 만들어진 터미널 스타일 차트를 문자 그대로 텍스트로 삽입합니다:
마크다운의 펜스 코드 블록, 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 코드 블록은 문자 그대로의 문자이며 다른 것은 남지 않습니다 (인라인 CSS, 스타일이 지정된 범위 없음). 차트가 필요한 모든 구분은 문자/숫자/범례에 있으며, 사용할 수 없는 색상 채널에 있지 않습니다.
차트 유형 메뉴
| 작업 | 함수 | 사용처 |
|---|---|---|
| 여러 범주, 모두 0에서 시작하는 크기 비교 | bar(value, max_value, width) + render() | 주유 (5분) vs. 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=) via render(bracket=) | "귀하 vs. 시장 중앙값" — 보고서의 특징적인 요소 |
| 1–5 (또는 유사) 평점 분포 | star_histogram(counts) | Google/Yelp 별점 분석, 별 개수당 한 행 |
| 두 범주형 축, 강도별 음영 | heatmap(matrix, row_labels, col_ticks=) | 요일 × 시간별 방문자 수 — "언제 바쁘고/한가한지" |
| 시간에 따른 추세, 하나의 시리즈 | line_chart(values, height, x_labels=) | 월별 수익, 1년간 리뷰 수 |
| 문장 내 인라인 추세 | sparkline(values) | "월별 리뷰: ▂▃▅▆█▇▅▃, 3월 이후 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 headless PNG 파이프라인으로 대체하는 것보다 더 나쁩니다.
차트를 위한 2초 테스트
좋은 문장과 마찬가지로 차트도 동일한 기준을 충족해야 합니다: 피곤한 독자가 휴대폰에서 첫눈에, 다시 읽거나 디코딩하지 않고 이해할 수 있어야 합니다. 차트 유형을 선택하기 전에 테스트를 적용하세요, 차트를 만든 후가 아니라.
2초 테스트: 의도된 독자 — 데이터 분석가가 아닌 소규모 사업주 — 가 범례를 먼저 읽지 않고 2초 이내에 이 차트가 무엇을 의미하는지 말할 수 있습니까? 하나의 영역이나 슬라이스를 이해하기 위해 먼저 범례를 디코딩해야 한다면, 그것은 전문 용어의 시각적 등가물입니다: 기술적으로 정확하지만 독자로 하여금 작업을 하게 만들고 앞으로 끌어당기지 않습니다.
실제 이슈에서 적발됨, 편집자가 렌더링된 포스트를 처음 읽을 때:
- 3-집합 벤 숫자-비트마스크 다이어그램은 어떤 영역이 의미하는지 이해하기 전에 7줄 범례를 읽어야 했습니다. 편집자: 이해하지 못했으므로 일반 사업주도 이해하지 못할 것입니다. 기본 사실 — "이 세 가지가 모두 필요하고 하나만으로는 안 된다" — 는 한 문장이며, 다이어그램 바로 다음의 번호 목록이 이미 일반 영어로 말하고 있었습니다. 다이어그램은 산문이 이미 더 명확하게 전달한 정보에 추가하지 않으면서 디코딩 비용을 추가했습니다. 수정: 잘라내고, 문장 + 목록이 작업을 수행하도록 하세요.
- DC 급속 충전기 비용 범위 막대 (축도, 비교 지점도 없는 단일 막대)는 비교할 대상이 없었습니다. 막대의 가치는 무언가에 상대적인 크기를 보여주는 데서 옵니다 — 척도, 다른 막대, 벤치마크. 참조 지점이 없는 단일 막대는 장식적일 뿐 정보를 제공하지 않습니다. 그것이 설명한 숫자($100k–$300k+)는 한 문장 앞서 이미 명확히 설명되어 있었습니다. 수정: 그것도 잘라내세요.
따르는 규칙:
- 읽기 위해 자체 범례가 필요한 차트는 경고 신호입니다 — 벤에만 해당하는 것이 아닙니다. 레이블이 없는 축이 있는 히트맵, 또는 독자가 머릿속에 담을 수 있는 것보다 더 많은 슬라이스가 있는 원형 차트도 같은 방식으로 실패합니다.
- 일반 문장이 차트만큼 명확하게 사실을 진술한다면 차트를 건너뛰세요. 산문에서 "여러/많은/다양한"을 실제 숫자로 바꾸는 것과 동일한 본능입니다: 비교 또는 추세가 핵심이기 때문에 시각적 요소를 사용하고, 데이터에 형태가 있다는 이유만으로 사용하지 마세요.
- 모든 막대에는 축(
render(..., axis=)) 또는 비교할 동일한 척도의 두 번째 막대가 필요합니다. 둘 다 없는 단일 막대는 아무것도 전달하지 않습니다 — 실제 주어가 없는 수동태 문장과 동일한 실패입니다. 그러나 이것이 충족되면, 축이 각 행의 값 레이블에 이미 있는 숫자를 반복할 뿐이라면 축을 제거하세요 — 하나의 가스-대-충전 막대는 정확히 이 이유로0 ... 45 min축 라인을 제거했습니다: 두 행 모두 이미 자체 값("5 min", "45 min")을 가지고 있으므로 축은 "45 min"을 두 번째로 반복했을 뿐입니다. 문장에서 중복 절을 자르는 것과 동일한 원칙입니다. - 이미 하나의 명확한 문장인 숫자는 차트로 만들지 마세요. "$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=("1월", "9월")))
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에서 불가능하다고 믿어졌습니다
(초기 게시물은 벤 다이어그램에 대해 headless-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으로 하드코딩되었으며
한 번도 조정되지 않았습니다. 이제 올바르게 측정된 2.49가 사용됩니다.
Sizing
render(..., width=)에 의해 처리되는 가로 너비: 모든 차트 구성 요소(레이블, 막대, 값 열)를
포함하는 행당 총 문자 수입니다. 390px 휴대폰 뷰포트에서 렌더링을 확인하세요.
세로 높이는 차트 유형에 따라 다릅니다:
- 막대/범위 막대/불릿: 행 수 = len(rows) (고정)
- 꺾은선 차트:
height=로 설정, 값 수에 따라 자동 조정 - 원형/도넛/산점도:
radius=로 설정, 차트 유형에 따라 해석
히트맵과 별 히스토그램은 데이터에서 자체 크기를 결정합니다.
차트 유형 선택
위의 메뉴와 2초 테스트를 안내로 사용하세요. 메시지에 맞지 않는 차트 유형을 강제하지 마세요 — 일반 텍스트가 작업을 명확하게 수행한다면 사용하세요.
추가 참고 사항
- 의존성이 없습니다:
scripts/ascii_chart.py는 표준 라이브러리만 사용합니다. - 차트는 완전히 결정론적입니다: 동일한 입력은 항상 동일한 출력을 생성합니다.
- 모든 차트 유형은 깊게 중첩된 구조 없이 단일 함수 호출로 생성됩니다.
- 오류 처리는 최소화되어 있습니다: 유효한 입력을 가정합니다.