一. 前言:圖表不是截圖,是探索工具 #
做資料分析時,靜態圖很夠用。 你畫一張折線圖,看趨勢。 你畫一張長條圖,看分類差異。 你畫一張散佈圖,看兩個欄位是不是有關係。
但只要資料稍微複雜一點,靜態圖就會開始卡。
- 想知道某個點的原始數值,要回去查表。
- 想切換地區或產品,只能重新跑一段程式。
- 想把圖丟給同事,對方沒有 Python 環境就打不開。
- 想做 Dashboard,又還不想直接開一個 Web app 專案。
這時候 Plotly 很好用。
Plotly 的定位不是「比較漂亮的 matplotlib」而已。 它真正香的地方是:圖表預設就能 hover、zoom、pan、選取、顯示圖例、輸出成 HTML。 也就是說,圖表本身就能承載一部分探索流程。
拍拍君之前在 Streamlit 入門篇 和 Streamlit 進階篇 都有提到 Plotly。 不過那兩篇重點是 Streamlit App。 今天這篇反過來:先不管 Web UI,專心把 Plotly 本身練順。
我們會用一份小型銷售資料,逐步做出 Plotly Express 圖表、好讀的 hover tooltip、篩選後作圖、subplots,以及可寄給別人的 HTML 報表。 學完以後,你就可以把 Plotly 當成資料探索的日常工具。 等哪天真的需要互動 App,再接 Streamlit、Dash 或其他前端都不遲。
二. 安裝:先把環境弄乾淨 #
先建立一個小專案。 拍拍君這裡用 uv;不用 uv 的話,傳統 venv 也可以。
uv init plotly-demo
cd plotly-demo
uv add plotly pandas numpy
如果你用一般虛擬環境:
python -m venv .venv
source .venv/bin/activate
pip install plotly pandas numpy
確認版本:
python -c "import plotly; print(plotly.__version__)"
Plotly 有兩個常用入口:plotly.express 是高階 API,適合快速從 DataFrame 畫圖;plotly.graph_objects 是低階 API,適合細部控制 trace、layout、subplots。
實務上不是二選一。拍拍君通常會先用 Plotly Express 做出第一版,如果要調得更細,再把它當成 Figure 物件繼續修改。
三. 準備範例資料:不要一開始就用玩具資料 #
我們先做一份小型銷售資料。 欄位包含日期、地區、產品、營收、訂單數、廣告花費。 這種資料很適合示範 Dashboard 圖表,因為它有時間、分類和數值。
建立 demo_data.py:
from __future__ import annotations
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
dates = pd.date_range("2026-01-01", periods=120, freq="D")
regions = ["北部", "中部", "南部", "東部"]
products = ["拍拍豆", "拍拍杯", "拍拍貼紙"]
rows = []
for date in dates:
for region in regions:
for product in products:
base = {"拍拍豆": 4200, "拍拍杯": 2600, "拍拍貼紙": 1200}[product]
factor = {"北部": 1.25, "中部": 1.0, "南部": 0.9, "東部": 0.55}[region]
revenue = max(200, base * factor * rng.normal(1.0, 0.12))
orders = max(1, int(revenue / rng.uniform(180, 340)))
rows.append({
"date": date,
"region": region,
"product": product,
"revenue": round(revenue, 2),
"orders": orders,
"ad_spend": round(revenue * rng.uniform(0.06, 0.18), 2),
})
pd.DataFrame(rows).to_csv("sales.csv", index=False)
執行:
uv run python demo_data.py
先看一下資料:
import pandas as pd
df = pd.read_csv("sales.csv", parse_dates=["date"])
print(df.head())
這裡有個小習慣很重要: 日期欄位要在讀取時轉成 datetime。 Plotly 可以處理字串日期,但 datetime 會讓排序、時間軸和 hover 顯示更穩。
四. Plotly Express:先用最短路徑畫出來 #
先從最常用的 Plotly Express 開始。
建立 first_chart.py:
import pandas as pd
import plotly.express as px
df = pd.read_csv("sales.csv", parse_dates=["date"])
daily = (
df.groupby("date", as_index=False)
.agg(revenue=("revenue", "sum"), orders=("orders", "sum"))
)
fig = px.line(
daily,
x="date",
y="revenue",
title="每日總營收",
markers=True,
)
fig.show()
執行:
uv run python first_chart.py
如果你在一般桌面環境,Plotly 會用瀏覽器打開互動圖。 你可以 hover 看數值、拖曳縮放、雙擊回復範圍。
Plotly Express 的核心直覺是:
- DataFrame 是資料來源。
- x、y、color、size、facet 這些參數是視覺編碼。
- 回傳值是 Figure,可以繼續修改。
所以要依產品分組,只要加上 color。
product_daily = (
df.groupby(["date", "product"], as_index=False)
.agg(revenue=("revenue", "sum"))
)
fig = px.line(
product_daily,
x="date",
y="revenue",
color="product",
title="各產品每日營收",
)
fig.show()
這比手動建立多條線容易很多。 而且圖例預設可以點擊開關,對探索資料很方便。
五. hover 資訊:讓圖表自己會說話 #
互動圖表最容易被低估的是 hover tooltip。 如果 hover 只顯示 x 和 y,那其實只比靜態圖多一點點互動。 真正好用的圖,hover 應該回答使用者下一秒想問的問題。
例如營收趨勢圖,使用者通常也想知道訂單數、廣告花費和平均客單價。
df["aov"] = df["revenue"] / df["orders"]
daily_region = (
df.groupby(["date", "region"], as_index=False)
.agg(
revenue=("revenue", "sum"),
orders=("orders", "sum"),
ad_spend=("ad_spend", "sum"),
aov=("aov", "mean"),
)
)
fig = px.line(
daily_region,
x="date",
y="revenue",
color="region",
title="各地區每日營收",
labels={
"date": "日期",
"revenue": "營收",
"region": "地區",
"orders": "訂單數",
"ad_spend": "廣告花費",
"aov": "平均客單價",
},
hover_data={
"orders": ":,",
"ad_spend": ":,.0f",
"aov": ":,.1f",
"revenue": ":,.0f",
},
)
fig.show()
labels 負責把欄位名稱翻成人類看得懂的字。
hover_data 負責決定哪些欄位出現在 tooltip 裡,以及數字格式。
拍拍君建議一開始就整理 labels。
因為圖表如果要給別人看,欄位名稱像 ad_spend、aov 這種工程內部縮寫,會讓讀者慢半拍。
六. 篩選資料:互動不一定要靠 UI 框架 #
很多人想到互動,就立刻想到 Streamlit 或 Dash。 但資料探索時,最重要的互動常常只是「先篩資料,再畫圖」。
filtered = df[
(df["region"].isin(["北部", "中部"]))
& (df["product"] == "拍拍豆")
& (df["date"] >= "2026-02-01")
]
trend = (
filtered.groupby(["date", "region"], as_index=False)
.agg(revenue=("revenue", "sum"))
)
fig = px.line(
trend,
x="date",
y="revenue",
color="region",
title="拍拍豆:北部與中部營收趨勢",
)
fig.update_layout(template="plotly_white", hovermode="x unified")
fig.show()
篩選條件未來可以來自 Streamlit sidebar、設定檔或 CLI 參數。 但畫圖函式不要直接綁死 UI,後面才好重用。
七. Subplots:把重點放在同一張報表裡 #
Dashboard 圖表不一定要每張圖分開。
如果你想把營收趨勢和地區比較放在同一個 HTML 裡,可以用 make_subplots。
from plotly.subplots import make_subplots
import plotly.graph_objects as go
daily = df.groupby("date", as_index=False).agg(revenue=("revenue", "sum"))
region = df.groupby("region", as_index=False).agg(revenue=("revenue", "sum"))
fig = make_subplots(
rows=1,
cols=2,
subplot_titles=("每日營收", "各地區營收"),
)
fig.add_trace(
go.Scatter(x=daily["date"], y=daily["revenue"], mode="lines", name="每日營收"),
row=1,
col=1,
)
fig.add_trace(
go.Bar(x=region["region"], y=region["revenue"], name="地區營收"),
row=1,
col=2,
)
fig.update_layout(
title="拍拍商店營運總覽",
template="plotly_white",
height=480,
showlegend=False,
)
fig.show()
Subplots 的優點是集中,缺點是每張小圖空間變少。 如果使用者需要比較趨勢,放一起很好。 如果每張圖都需要仔細 hover 和縮放,分開反而比較舒服。
八. 匯出 HTML:讓圖表離開你的電腦 #
Plotly 很實用的一點是可以直接輸出 HTML。 這樣對方不用裝 Python,也能打開互動圖。
fig.write_html(
"sales-dashboard.html",
include_plotlyjs="cdn",
full_html=True,
)
“cdn” 會讓 HTML 比較小,但打開時需要網路。
如果要寄給不能確定網路環境的人,可以把 include_plotlyjs 改成 True,讓檔案離線可開。
也可以輸出靜態圖片。 這需要 kaleido:
uv add kaleido
fig.write_image("sales-dashboard.png", scale=2)
靜態圖適合簡報、文件、社群貼文。 互動 HTML 適合探索、內部報表和可點擊的分析附件。
九. 常見坑:Plotly 很強,但不要亂用 #
Plotly 最常見的坑不是語法,而是資料量和圖表設計。
第一,點太多會卡;幾十萬個點應該先聚合、抽樣或篩選,不要全部丟進瀏覽器。
第二,類別太多會亂;如果 color 有 80 種分類,圖例會變成災難,先保留前幾名,把其他合併成「其他」。
第三,雙 y 軸要節制。
雙 y 軸很方便,但也很容易誤導。
只有在兩個指標真的需要同時看趨勢,且單位差異明確時才用。
第四,hover 不要塞成履歷表。 tooltip 應該補足圖表,不是把整列資料全倒出來。 一個好 hover 通常 3 到 6 個欄位就夠。
第五,圖表標題要寫結論,不只寫欄位。 「每日營收」可以用。 但「北部週末營收明顯高於平日」更有分析價值。
結語 #
Plotly 很適合放在 Python 資料工具箱裡。 它不像完整 BI 工具那樣需要一堆設定,也不像靜態圖那樣只能看不能摸。 它剛好站在中間:用 Python 寫、用瀏覽器互動、用 HTML 分享。
今天的重點可以濃縮成幾句:
- 快速探索先用 Plotly Express。
- 需要細部控制再用 Graph Objects。
- hover、labels、template 會大幅影響圖表可讀性。
- 資料篩選和畫圖邏輯要分開,之後才好接 App。
- 匯出 HTML 是 Plotly 的大招,內部報表超好用。
下次你做完一份資料分析,先不要急著截圖。 試著把圖表做成可以 hover、zoom、分享的互動 HTML。 讀者能自己探索,圖表就不只是結果,而是工具。