storytelling-viz:可直接用的圖
目標是一張能直接放進彙報、文件或期刊的圖,而不是隨手畫的長條圖。它要滿足三點:
- 圖形對。 圖形與要比較的東西匹配,焦點清楚,直接標註,不用圖例。
- 在真實資料上不崩。 長標籤、負值、缺失、類別多、離群值都能正確顯示。
- 符合輸出規格。 尺寸、字型大小、線寬、格式、字型嵌入都合規。
本 skill 只管圖。選題、分析和報告結構不在本 skill 範圍內。
工作流程
- 判斷場景。 場景決定顏色、註腳、數字等預設慣例(見「兩種場景的慣例」):
- 經營彙報:給業務方或管理層看的經營指標圖,不論頻率和場合(週報、月報、日報看板、異動歸因、管理層 / 董事會 / 戰略會的經營資料頁)。判斷依據是「內容是不是經營指標、讀者是不是要據此做業務決策」,不是「是不是週報」。
- 通用 / 出版:分析或研究文件、實驗結果、網頁、論文配圖等。
- 拿不定時按經營彙報處理(慣例更嚴,只多一行註腳)。經營彙報場景必須先讀
references/business.md,即使範例程式碼看起來已經夠用。
- 按「要比較什麼」選函數。 見下方選函數表;兩種場景用同一張表。
- 選輸出規格。 用
spec=指定(幻燈片、文件、期刊……)。 - 出圖,看自動檢查結果。
figure()/save()渲染完會印出QA … ok或列出每條警告;回傳值的.qa是同一份問題清單(空清單 = 通過)。有警告就改,不交付。 - 開啟 PNG 按檢查清單檢查。 用 Read 開啟產生的 PNG,按文末的檢查清單逐條核對。
快速開始
# /// script
# dependencies = ["plotnine>=0.15,<0.16", "pandas", "pillow", "pypdf"]
# ///
import sys; sys.path.insert(0, "<skill>/scripts")
from viz_common import figure, titled, set_lang, save
import charts as C # 通用圖表函數
import biz as B # 經營彙報圖表函數(與 C 混用無妨)
set_lang("en") # 或 "zh"(預設)
c = C.bar_rank(df, "channel", "gmv", highlight="POS", top_n=8, fmt="num")
figure([[(titled(c, title="POS is the only channel that shrank"), 1)]],
spec="doc", name="fig1", outdir="out")
save(c, "fig2", spec="journal-1col", outdir="out") # 期刊單圖:不加標題
# 經營彙報:同樣的 figure(),但必須寫口徑註腳
k = B.kpi_cards([dict(label="GMV (MTD)", value=16.5e6, prev=15.9e6, target=57e6, progress=0.29)])
figure([[(k, 1)]], spec="doc", size=(6.5, 2), name="kpi", outdir="out",
caption="Basis: paid-order GMV (USD); MTD to Oct 9 vs Sep 1–9.")
選函數
按要比較的東西找行。C. = charts.py,B. = biz.py;標 ★ 的是為經營彙報設計的函數,但任何場景都能用。
| 要比較的 | 函數 | 重點 |
|---|---|---|
| 排名、一個時點的構成、正負值 | C.bar_rank(df, cat, value, highlight=, top_n=, fmt=) | 自動排序;超過 top_n 合併為「其他/Other」;值寫在條內或條外 |
| 誰帶來了變化(分維度的規模 + 增速 + 貢獻) | ★ B.contrib_table(df, dim, prev, cur, top_n=, growth_alert=) | 規模資料條、▲▼ 增速、變化量、佔總變化;合計行置頂 |
| 趨勢:1–N 條線放一起 | C.line_trend(df, x, y, group=, highlight=, band=, ref=) | 線尾直接標註;按月自動兩層月份標籤;缺失斷線;band= 信賴帶 |
| 趨勢:多組分開看 | C.small_multiples(df, x, y, facet, highlight=) | 所有格子同一 y 刻度;灰色背景線是其他組 |
| 趨勢:一個經營指標 + 同期對比 | ★ B.trend(df, date, value, compare="month"/"year", events=, as_of=, period_end=) | 自動疊加對齊好的同期線;標註事件;未完結時期虛線 + 預估 |
| 進度:期內累計 vs 目標 | ★ B.cumulative_vs_target(df, date, value, target, start, end, as_of=, prior=) | 累計實際、目標進度線、上期累計、按日均折算的預估 |
| 實際 vs 目標(多行) | ★ B.bullet(df, cat, actual, target, progress=) | 實際條 + 目標豎線 + 「按進度應完成」虛線;落後進度的行變紅 |
| 實際 vs 區間(預測區間、正常範圍) | C.range_dot(df, cat, low, high, value, flag="below", bench=) | 越界的行用焦點色;bench= 指定基準行 |
| 每組兩個值(前後、計畫與實際) | C.dumbbell(df, cat, start, end, labels=, highlight=) | 空心點 = 起點,實心點 = 終點;按終點排序;每行帶變化率 |
| 首尾兩期,各組規模差異大 | C.slope(df, cat, start, end, mode="change", groups=) | mode="change" 時所有線從 0% 出發;起點為 0 的行剔除並打 WARNING |
| 增減橋接:上期 → 各項貢獻 → 本期 | C.waterfall(df, cat, delta, start=, end_label=) | 增減預設中性色 + 正負號;好壞確有業務意義時傳 up=PAL.good, down=PAL.bad |
| 指標拆解(GMV = 使用者 × 頻次 × 客單價) | ★ B.driver_tree(nodes) | 每個節點寫目前值、▲▼ 變化、貢獻(乘法用對數份額,加法用變化量) |
| 構成:多組各自的佔比 | C.stacked_100(df, cat, part, value, order=, focus_cat=) | 部分按自然順序、單色漸變;圖例行在 chart.legend |
| 二維表(組 × 時間、組 × 組) | C.heatmap(df, x, y, value, fmt=) | 白到單色,色階取 2%–98% 分位;深色格白字 |
| 規模 + 比率(申請數與通過率) | ★ B.bar_line_dual(df, x, bar, line, line_range=) | 唯一允許的雙軸:規模柱(左)+ 比率線(右),比率軸從 0 或固定範圍 |
| 分佈 | C.distribution(df, value, group=, kind="box"/"strip"/"hist") | 按中位數排序;鬚線 1.5×IQR;點多時抽樣 |
| 兩個連續變數的關係 | C.scatter(df, x, y, label=, fit="linear", groups=, x_title=, y_title=) | OLS + 信賴帶,顯示 R²;只標高亮的點;groups= 時圖例在 chart.legend |
| 估計值與信賴區間 | C.point_range(df, cat, est, low, high, ref=0) | 森林圖;右側寫「估計值 [下限, 上限]」 |
| 幾個關鍵數字的摘要(報表頂部) | ★ B.kpi_cards([dict(label, value, prev, target, progress, spark, …), …]) | 大數字、▲▼ 變化、達成率與時間進度、迷你趨勢線;異常才變紅 |
★ 函數的參數細節(kpi_cards 的欄位、driver_tree 的節點結構、trend 的 events= / as_of=、cumulative_vs_target 的 prior=、異常規則、同期對齊方式)見 references/business.md。
兩行都像時怎麼選:
B.contrib_tablevsC.waterfall:要同時看規模、增速和佔比,或維度超過約 8 個 → 表;要講「上期怎麼一步步變成長這期」、項目不超過約 8 個 → 瀑布圖。歸因頁常兩個並排。C.line_trendvsC.small_multiples:有一條要突出的線、或不超過 4 條 →line_trend(超過 4 條加highlight=);要逐組看形狀、各組地位相當 →small_multiples。期刊單欄(3.5in)放不下 5 個以上分面,用line_trend+highlight=。- 每組兩個值,先看各組規模:相近(最大 / 最小 < 約 10 倍)→
C.dumbbell。相差一個數量級以上時,小組在共同刻度上看不清,改為直接畫變化率:不超過約 8 組 →C.slope(mode="change");更多組 → 先算出變化率列,用C.bar_rank(df, cat, "chg", fmt="pct"),絕對規模寫進標籤或旁邊的表。
所有函數共用的參數和回傳值:
highlight=:類別名、清單或函數。被選中的項用焦點色,其餘變灰,類別名也會同色加粗。fmt=:"num"、"pct"、"pp"、"int"、"raw"、格式字串或函數;nd=控制小數位。- 回傳的是面板物件,直接放進
figure()/save();chart + plotnine層可繼續疊加;chart.legend是一行彩色詞圖例,titled()會自動用上。
沒有合適的函數時,自己畫:
- 用
C.Frame(w, h, xr, yr, left=…, …)按文字的實際寬度(text_width)預留四周空白;軸範圍用C.axis_range(lo, hi)。 - 在
C.Canvas()上用rect、seg、pt、text、line、band畫元素,用cv.plot(fr)產生圖。 - 包成
C.Chart(build),其中build(w, h)在拿到最終尺寸後才計算佈局。
不要在程式碼裡寫死座標或軸範圍。
兩種場景的慣例
函數本身在兩種場景下畫法一樣;不同的是這些預設慣例。程式碼裡沒有「場景」參數:★ 函數自帶經營彙報的畫法(▲▼、異常才著紅),並會讓 figure() 在缺口徑註腳時報 WARNING;同一頁上的 C. 圖則由你按下表的慣例傳參(如 caption=、highlight=、up= / down= 的顏色)。
| 項 | 通用 / 出版 | 經營彙報 |
|---|---|---|
| 漲跌 | 好壞確有意義時才用 good / bad,且必須配正負號或文字 | 一律 ▲▼ + 中性色,不用紅綠表示漲跌 |
| 什麼時候著紅 | 只在表示「壞」時 | 只有被判為異常的(落後進度 / 超閾值 / 超出歷史 ±2σ),好方向的異常只加粗 |
| 要讀者「看這裡」(如拖累成長的渠道) | highlight=(焦點色橙) | 同左;橙色只表示「看這裡」,不表示好壞,是否異常仍由 ▲▼ 和紅色表達 |
| 口徑註腳 | 建議寫 | 必填(caption=):分子分母、幣種、時間窗口、對比基準;缺了 figure() 會報 WARNING |
| 對比基準 | 按分析需要 | 預設月環比、本月累計 vs 上月同期(同樣天數)、目標達成;日度流量指標按星期對齊同期 |
| 未完結的當期 | 淺色或虛線 + 「截至 X 日」 | 同左,並畫按進度折算的全期預估 |
| 數字 | 按領域習慣 | 金額 K/M/B;比率變化用 pp,增速用 %;單位寫在標題或註解裡 |
| 雙軸 | 不用 | 只允許「規模柱 + 比率線」(B.bar_line_dual) |
| 圓餅圖 / 環圈圖 | 盡量不用 | 禁用 |
| 常用規格 | doc、web、journal-* | PPT 用 slide;飛書 / Lark / Notion 文件或聊天裡用 doc(PNG 插圖);使用者沒說放哪時用 doc |
原因與反例見 references/rules.md(經營彙報部分是 B1–B7)。
輸出規格(spec=)
| spec | 尺寸(英寸) | 正文字型大小 | 預設格式 | 用途 |
|---|---|---|---|---|
slide / slide-4x3 | 13.33×7.5 / 10×7.5 | 12 / 11.5 | 輪廓 SVG、可編輯 SVG、PNG(150dpi) | 彙報幻燈片 |
web | 8×4.5 | 10.5 | SVG、PNG(200dpi) | 網頁、郵件 |
doc | 6.5×4 | 9 | PDF、SVG、PNG(300dpi) | 文件正文裡的一張圖 |
a4 / letter | 整頁 | 10 | PDF、SVG、PNG | 整頁報告 |
journal-1col / -1.5col / -2col | 3.5 / 5.35 / 7.2 寬 | 7–7.5 | PDF、SVG、PNG(600dpi) | 期刊(89 / 136 / 183 mm) |
size=(w, h)可以覆蓋預設尺寸,比如更高的期刊圖、更矮的 KPI 卡片行。- 字型大小、線寬和點的大小都會隨規格縮放;PDF 用 Type 42 嵌入字型,滿足期刊要求。
- 期刊圖不加標題,圖註寫在正文裡:單張圖用
save(chart, name, spec="journal-1col");多面板圖用figure(rows, spec="journal-2col"),不傳title=,面板標號用titled(chart, title="a")。 figure()出一整頁(可有標題、副標題、口徑註腳、多個面板);save(chart, "fig2", spec="journal-1col")只導出一張圖,沒有頁標題和註腳。經營彙報要註腳,所以用figure()。- 兩種 SVG:
<name>.svg的文字轉成了輪廓,任何地方開啟都一樣;<name>_editable.svg保留文字,可在 Figma / PPT 裡改字,但對方電腦要有同樣的字型。PPT 裡要改字用後者,否則用前者或 PNG。 slide/web預設不出 PDF,需要時傳formats=("pdf", "png")。
組合多面板
figure(rows, title=, subtitle=, caption=, spec=):rows=[[(panel, span), …], …],span是同一行內的相對寬度。- 行高預設自動分配:只放文字或表格的行按內容取高度,其餘行平分剩下的空間。
titled(chart, title=, legend=, note=):在圖上方加面板標題、圖例行和註解;legend不填時自動用chart.legend。- 標題支援富文字標記:
**粗體**;[短語](focus)表示加粗並著色。顏色可以寫focus、brand、good、bad、muted、ink或#hex。 - 其他面板:
text_panel(para(...))放段落,table_panel(rows, header=)放小表(數字列自動右對齊),column(a, b)把兩個面板上下疊放。 - 每個面板都按最終尺寸單獨渲染,所以字型大小就是實際磅值;拼接 SVG 時會自動給各面板的元素 id 加前綴,避免衝突。
視覺規則(函數已預設執行,自己畫圖時照做)
- 顏色分層:
focus(橙)標出唯一要看的东西,本身不代表好壞;brand(深藍)用於常規系列;灰色用於背景和參照;good/bad的用法按場景(見上表)。 - 同色即同義。 文字關鍵詞與對應資料用同一種顏色;意義不同的元素不要共用顏色。
- 文字要讀得清。 文字對比度至少 4.5:1。焦點色做文字時用
readable(color)自動加深;深色底上的字用ink_on(fill)。函數會自動處理,自己畫圖時Canvas.text也會檢查並警告。 - 用直接標註代替圖例。 軸標題不能是欄位名。所有柱子都標了數值時,就不要 y 軸和網格。
- 排序要有意義。 有自然順序的(階段、等級)按自然順序,否則按數值排序;焦點項放在最顯眼的位置;不要按字母序。
- 座標軸。 長條圖必須從 0 開始;折線圖資料靠近 0 時從 0 開始(
zero="auto");相關的幾個面板用同一刻度;雙軸只有「規模柱 + 比率線」一種。 - 精度。 夠區分就行。數字一律由資料計算,用
pct/pp/num/fmt_value格式化,不要手寫。 - 按要比較的東西選圖: 佔比變化不要用絕對值堆積柱;多組構成不要畫多個圓餅圖;超過 4 條線時用
highlight=突出重點或改用小多圖;樹狀圖、雷達圖比較兩項要超過約 2 秒就換成橫條。 - 資料問題要在圖上標出。 未完結的當期用淺色或虛線並註明「截至 X 日」;小樣本寫出 n;缺失時斷線,不要連起來。
細節與原因見 references/rules.md;SWD 改圖案例的設計做法見 references/swd_practices.md。
語言與排版
set_lang("zh" | "en")會同時切換預設字型、月份標籤(中文「1月\n2026」,英文「Jan\n2026」)和「其他/Other」。金額單位兩種語言都預設 K/M/B,需要萬/億時用set_number_style("cn")。含中文的標籤自動改用 CJK 字型。T("其他", "Other")按目前語言取字串。- 中文字串會自動經過
zh():標點轉為全形,去掉全形標點旁多餘的空格,網址和縮寫保持不變。 - 換行規則:數字和英文單詞不拆開;標點不出現在行首;左括號不留在行尾。
- 啟動時會印出所用字型。出現
WARNING: no CJK font時需要安裝 Noto Sans SC。
交付前檢查
自動檢查(看 figure() 的輸出或 .qa):
-
QA … ok:文字沒有重疊、沒有跑出面板、沒有被繪圖區裁切。 - 沒有對比度警告,也沒有
rows do not fit、labels take most of the panel、cells too small、basis caption這類警告。
人工檢查(開啟 PNG):
- 遮住標題,只看圖,能說出要表達的內容嗎?焦點只有一個嗎?
- 標題和標註裡的數字,與圖上的數值來自同一個變數嗎?
- 圖形匹配要比較的東西嗎(見選函數表)?排序有意義嗎?相關面板刻度一致嗎?
- 顏色用法符合場景慣例嗎?去掉顏色後,好壞 / 漲跌還能從符號或文字讀出來嗎?
- 沒有預設圖例、欄位名、3D 效果或漸層嗎?
- 尺寸和字型大小符合目標規格嗎(期刊正文 7pt 左右,幻燈片 12pt 以上)?需要 PDF 的已經導出了嗎?
經營彙報另加:
- 口徑註腳寫全了嗎?和其他報表對過數嗎(同一份報告裡,同一個數在各頁一致)?
- 單位、小數位、格式全圖統一嗎?
環境與測試
- 依賴:
plotnine>=0.15,<0.16(已在 0.15.8 + matplotlib 3.11 下驗證)、pandas、pillow,匯出 PDF 還要pypdf。程式碼在scripts/viz_common.py(底層工具、規格、匯出、QA)、scripts/charts.py(C.)和scripts/biz.py(B.)。 - 輸出位置: 把
outdir設成使用者能看到的目錄;不要寫進 skill 目錄本身。 - 範例圖庫:
uv run references/gallery.py [outdir](每個C.函數,中文幻燈片 + 英文期刊);uv run references/business_gallery.py [outdir](每個B.函數,中文幻燈片 + 英文文件)。預設輸出到目前目錄的out/。 - 回歸測試:
uv run references/tests/edge_cases.py用 38 組邊界資料,在兩種規格下(共 76 項)檢查函數的表現和 QA 是否符合預期;改了圖表函數後必須重跑,全部通過才能發布。 - 沙箱裡裝不上 plotnine 時: 先用
PYTHONPATH=<skill>/scripts/plotnine_preview的 matplotlib 模擬器預覽版式和跑 QA(線寬和裁切規則與 plotnine 一致)。模擬器的QA ok不算交付標準:把腳本交給使用者本機uv run,並明確告訴使用者這張圖還沒有用真 plotnine 渲染和檢查過。