Plotly 互動式圖表指南
Plotly 圖表教學指南
Plotly 是一個功能強大的 Python 資料視覺化函式庫,支援互動式與動態圖表,尤其適合用於網頁應用和資料分析。相較於 Matplotlib,Plotly 提供更豐富的互動性和美觀的預設樣式,能夠輕鬆製作出專業級的資料視覺化圖表。
本教學將從基礎到進階,示範如何使用 Plotly 繪製各種類型的圖表,並特別聚焦於如何從 JSON 資料中建立視覺化。
目錄
基礎設定
首先,我們需要安裝並匯入必要的套件:
# 安裝套件
# pip install plotly pandas numpy
# 匯入套件
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
import json
Plotly 提供兩種主要的 API:
- Plotly Express (px):高階 API,簡單易用,適合快速建立常見圖表。
- Graph Objects (go):低階 API,提供更精細的控制,適合自訂需求較高的圖表。
從 JSON 資料入手
我們先準備一個 JSON 資料檔案作為示範:
# 示範 JSON 資料
data = {
"sales": [
{"month": "Jan", "2022": 1200, "2023": 1350, "2024": 1500},
{"month": "Feb", "2022": 1100, "2023": 1400, "2024": 1550},
{"month": "Mar", "2022": 1300, "2023": 1450, "2024": 1600},
{"month": "Apr", "2022": 1400, "2023": 1500, "2024": 1650},
{"month": "May", "2022": 1500, "2023": 1550, "2024": 1700},
{"month": "Jun", "2022": 1600, "2023": 1600, "2024": 1750}
],
"products": [
{"name": "Product A", "sales": 5000, "cost": 3000, "profit": 2000, "category": "Electronics"},
{"name": "Product B", "sales": 4500, "cost": 2500, "profit": 2000, "category": "Clothing"},
{"name": "Product C", "sales": 3500, "cost": 1500, "profit": 2000, "category": "Food"},
{"name": "Product D", "sales": 3000, "cost": 1000, "profit": 2000, "category": "Electronics"},
{"name": "Product E", "sales": 2500, "cost": 1200, "profit": 1300, "category": "Clothing"}
],
"visitors": {
"daily": [150, 180, 200, 220, 190, 210, 230],
"mobile": [85, 100, 120, 130, 110, 120, 140],
"desktop": [65, 80, 80, 90, 80, 90, 90],
"days": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
}
}
# 將資料轉換為 JSON 格式並儲存
with open('sample_data.json', 'w') as f:
json.dump(data, f)
# 從 JSON 檔案讀取資料
with open('sample_data.json', 'r') as f:
data = json.load(f)
基本圖表類型
1. 折線圖 - 顯示月份銷售趨勢
# 從 JSON 資料中提取月份銷售資料
sales_data = pd.DataFrame(data["sales"])
sales_data.set_index("month", inplace=True)
# 使用 Plotly Express 繪製折線圖
fig = px.line(sales_data, x=sales_data.index, y=["2022", "2023", "2024"],
title="月份銷售趨勢比較",
labels={"value": "銷售額", "variable": "年份"},
markers=True)
fig.update_layout(xaxis_title="月份", yaxis_title="銷售額")
fig.show()
2. 長條圖 - 產品銷售比較
# 從 JSON 資料中提取產品資料
products_df = pd.DataFrame(data["products"])
# 使用 Plotly Express 繪製長條圖
fig = px.bar(products_df, x="name", y="sales", color="category",
title="產品銷售比較",
labels={"name": "產品名稱", "sales": "銷售額", "category": "分類"})
fig.update_layout(xaxis_title="產品", yaxis_title="銷售額")
fig.show()
3. 圓餅圖 - 產品銷售佔比
# 使用 Plotly Express 繪製圓餅圖
fig = px.pie(products_df, values="sales", names="name",
title="產品銷售佔比",
hover_data=["category"])
fig.update_traces(textposition="inside", textinfo="percent+label")
fig.update_layout(uniformtext_minsize=12, uniformtext_mode="hide")
fig.show()
4. 散佈圖 - 產品銷售成本關係
# 使用 Plotly Express 繪製散佈圖
fig = px.scatter(products_df, x="cost", y="sales", color="category", size="profit",
hover_data=["name"],
title="產品銷售與成本關係",
labels={"cost": "成本", "sales": "銷售額", "profit": "利潤", "category": "分類"})
fig.update_layout(xaxis_title="成本", yaxis_title="銷售額")
fig.show()
進階圖表
5. 折線圖 + 柱狀圖 - 訪客數據分析
# 從 JSON 資料中提取訪客資料
visitors_df = pd.DataFrame({
"day": data["visitors"]["days"],
"total": data["visitors"]["daily"],
"mobile": data["visitors"]["mobile"],
"desktop": data["visitors"]["desktop"]
})
# 使用 Graph Objects 建立組合圖表
fig = go.Figure()
# 添加折線圖 - 總訪客數
fig.add_trace(go.Scatter(
x=visitors_df["day"],
y=visitors_df["total"],
mode="lines+markers",
name="總訪客數"
))
# 添加柱狀圖 - 行動裝置訪客
fig.add_trace(go.Bar(
x=visitors_df["day"],
y=visitors_df["mobile"],
name="行動裝置",
marker_color="orange"
))
# 添加柱狀圖 - 桌面裝置訪客
fig.add_trace(go.Bar(
x=visitors_df["day"],
y=visitors_df["desktop"],
name="桌面裝置",
marker_color="lightblue"
))
# 更新布局
fig.update_layout(
title="每日訪客數據分析",
xaxis_title="星期",
yaxis_title="訪客數",
barmode="stack"
)
fig.show()
6. 熱力圖 - 銷售數據熱力圖
# 重新格式化銷售資料以便繪製熱力圖
sales_matrix = sales_data.values.T
years = ["2022", "2023", "2024"]
months = sales_data.index
# 使用 Graph Objects 繪製熱力圖
fig = go.Figure(data=go.Heatmap(
z=sales_matrix,
x=months,
y=years,
colorscale="Viridis",
hoverongaps=False,
text=sales_matrix,
texttemplate="%{text}",
colorbar=dict(title="銷售額")
))
fig.update_layout(
title="銷售數據熱力圖",
xaxis_title="月份",
yaxis_title="年份"
)
fig.show()
7. 雷達圖 - 產品多維度比較
# 選擇幾個產品進行比較
categories = ["sales", "cost", "profit"]
product_names = products_df["name"].tolist()
# 使用 Graph Objects 繪製雷達圖
fig = go.Figure()
for i, product in enumerate(product_names):
product_data = products_df[products_df["name"] == product]
values = product_data[categories].values.flatten().tolist()
# 添加循環值使雷達圖閉合
values.append(values[0])
fig.add_trace(go.Scatterpolar(
r=values,
theta=categories + [categories[0]], # 添加第一個類別以閉合
fill="toself",
name=product
))
fig.update_layout(
title="產品多維度比較",
polar=dict(
radialaxis=dict(
visible=True,
range=[0, max(products_df["sales"].max(), products_df["cost"].max(), products_df["profit"].max())]
)
)
)
fig.show()
組合與交互式圖表
8. 子圖表 - 多個圖表組合
# 使用 make_subplots 創建多個子圖表
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("月份銷售趨勢", "產品銷售比較", "產品銷售佔比", "每日訪客數據"),
specs=[[{"type": "scatter"}, {"type": "bar"}],
[{"type": "pie"}, {"type": "scatter"}]]
)
# 添加第一個子圖表(折線圖)
for year in ["2022", "2023", "2024"]:
fig.add_trace(
go.Scatter(x=sales_data.index, y=sales_data[year], name=year, mode="lines+markers"),
row=1, col=1
)
# 添加第二個子圖表(長條圖)
fig.add_trace(
go.Bar(x=products_df["name"], y=products_df["sales"], name="產品銷售"),
row=1, col=2
)
# 添加第三個子圖表(圓餅圖)
fig.add_trace(
go.Pie(labels=products_df["name"], values=products_df["sales"], name="銷售佔比"),
row=2, col=1
)
# 添加第四個子圖表(折線圖 - 訪客數據)
fig.add_trace(
go.Scatter(x=visitors_df["day"], y=visitors_df["total"], name="總訪客數", mode="lines+markers"),
row=2, col=2
)
# 更新布局
fig.update_layout(
title_text="多圖表組合分析",
height=800,
showlegend=False
)
fig.show()
9. 交互式下拉選單 - 動態顯示不同年份的資料
# 從 JSON 資料中提取月份銷售資料
sales_data = pd.DataFrame(data["sales"])
# 使用 Graph Objects 創建交互式圖表
fig = go.Figure()
# 添加三條折線,但只顯示 2024 年資料
fig.add_trace(go.Scatter(
x=sales_data["month"],
y=sales_data["2022"],
mode="lines+markers",
name="2022",
visible=False
))
fig.add_trace(go.Scatter(
x=sales_data["month"],
y=sales_data["2023"],
mode="lines+markers",
name="2023",
visible=False
))
fig.add_trace(go.Scatter(
x=sales_data["month"],
y=sales_data["2024"],
mode="lines+markers",
name="2024",
visible=True
))
# 更新布局和添加下拉選單
fig.update_layout(
title="月份銷售趨勢 (可選擇年份)",
xaxis_title="月份",
yaxis_title="銷售額",
updatemenus=[
dict(
buttons=list([
dict(
args=[{"visible": [True, False, False]}],
label="2022",
method="update"
),
dict(
args=[{"visible": [False, True, False]}],
label="2023",
method="update"
),
dict(
args=[{"visible": [False, False, True]}],
label="2024",
method="update"
)
]),
direction="down",
pad={"r": 10, "t": 10},
showactive=True,
x=0.1,
xanchor="left",
y=1.1,
yanchor="top"
),
]
)
fig.show()
自訂樣式與佈局
從自訂樣式部分繼續:
10. 自訂樣式 - 設計企業主題
# 自訂一個公司主題
company_colors = {
'primary': '#0066cc',
'secondary': '#ff9900',
'background': '#f2f2f2',
'text': '#333333'
}
# 使用 Graph Objects 繪製帶有自訂樣式的圖表
fig = go.Figure()
# 添加銷售資料比較
for year, color in zip(["2022", "2023", "2024"],
[company_colors['primary'], company_colors['secondary'], '#33aa66']):
fig.add_trace(go.Scatter(
x=sales_data.index,
y=sales_data[year],
mode="lines+markers",
name=year,
line=dict(color=color, width=3),
marker=dict(size=10, line=dict(width=2, color='white'))
))
# 更新圖表佈局和樣式
fig.update_layout(
title={
'text': "月份銷售趨勢比較",
'y':0.95,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top',
'font': dict(size=24, color=company_colors['text'], family="Arial, sans-serif")
},
plot_bgcolor=company_colors['background'],
paper_bgcolor='white',
xaxis=dict(
title="月份",
titlefont=dict(size=18, color=company_colors['text']),
showgrid=True,
gridcolor='lightgray',
tickfont=dict(size=14)
),
yaxis=dict(
title="銷售額",
titlefont=dict(size=18, color=company_colors['text']),
showgrid=True,
gridcolor='lightgray',
tickfont=dict(size=14)
),
legend=dict(
bgcolor='rgba(255,255,255,0.8)',
bordercolor=company_colors['primary'],
borderwidth=1,
font=dict(size=12)
),
hovermode="closest"
)
# 添加浮水印
fig.add_annotation(
text="Company Report 2024",
xref="paper", yref="paper",
x=0.5, y=0.05,
showarrow=False,
font=dict(size=12, color="lightgray"),
opacity=0.7
)
fig.show()
11. 自適應佈局 - 響應式設計
# 創建響應式圖表
fig = px.bar(
products_df,
x="name",
y="sales",
color="category",
title="產品銷售比較 (響應式設計)"
)
# 設定自適應佈局
fig.update_layout(
autosize=True,
margin=dict(l=50, r=50, b=100, t=100, pad=4),
height=None, # 自動調整高度
title={
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top',
}
)
fig.show()
12. 動畫效果 - 添加框架動畫
# 創建包含多個月份的資料集進行動畫展示
animation_df = pd.DataFrame()
# 逐月累積資料
for i, month in enumerate(sales_data.index):
temp_df = sales_data.iloc[:i+1].sum()
temp_df = pd.DataFrame(temp_df).T
temp_df['month'] = month
animation_df = pd.concat([animation_df, temp_df], ignore_index=True)
# 將資料進行轉置以便於動畫化
animation_df_melted = pd.melt(
animation_df,
id_vars='month',
value_vars=['2022', '2023', '2024'],
var_name='year',
value_name='cumulative_sales'
)
# 創建動畫圖表
fig = px.bar(
animation_df_melted,
x="year",
y="cumulative_sales",
color="year",
animation_frame="month",
title="月份累積銷售額 (動畫效果)",
labels={"cumulative_sales": "累積銷售額", "year": "年份", "month": "月份"},
range_y=[0, animation_df_melted['cumulative_sales'].max() * 1.1]
)
fig.update_layout(
xaxis_title="年份",
yaxis_title="累積銷售額"
)
fig.show()
匯出與分享
13. 匯出靜態圖片
# 匯出為靜態圖片
fig = px.line(sales_data, x=sales_data.index, y=["2022", "2023", "2024"],
title="月份銷售趨勢比較",
labels={"value": "銷售額", "variable": "年份"},
markers=True)
# 匯出為 PNG
fig.write_image("sales_trend.png", scale=2) # scale=2 提高解析度
# 匯出為 PDF
fig.write_image("sales_trend.pdf")
# 匯出為 SVG (向量圖形)
fig.write_image("sales_trend.svg")
14. 匯出互動式 HTML
# 匯出為互動式 HTML 檔案
fig = px.scatter(products_df, x="cost", y="sales", color="category", size="profit",
hover_data=["name"],
title="產品銷售與成本關係",
labels={"cost": "成本", "sales": "銷售額", "profit": "利潤", "category": "分類"})
# 完整檔案匯出
fig.write_html("sales_vs_cost.html")
# 輕量化版本 (較小檔案大小)
fig.write_html("sales_vs_cost_light.html", include_plotlyjs='cdn')
15. 整合至 Dash 儀表板
Plotly 的 Dash 框架可讓您輕鬆建立交互式的網頁儀表板。以下是一個簡單的 Dash 應用範例:
# pip install dash
from dash import Dash, html, dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
# 初始化 Dash 應用
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
# 建立銷售圖表
def create_sales_chart():
return px.line(sales_data, x=sales_data.index, y=["2022", "2023", "2024"],
title="月份銷售趨勢比較",
labels={"value": "銷售額", "variable": "年份"},
markers=True)
# 建立產品圖表
def create_product_chart():
return px.bar(products_df, x="name", y="sales", color="category",
title="產品銷售比較",
labels={"name": "產品名稱", "sales": "銷售額", "category": "分類"})
# 應用佈局
app.layout = dbc.Container([
html.H1("銷售數據儀表板", className="mt-4 mb-4 text-center"),
dbc.Row([
dbc.Col([
html.Div([
html.H4("年份選擇"),
dcc.Dropdown(
id="year-dropdown",
options=[
{"label": "2022", "value": "2022"},
{"label": "2023", "value": "2023"},
{"label": "2024", "value": "2024"},
{"label": "所有年份", "value": "all"}
],
value="all",
clearable=False
)
], className="mb-4")
])
]),
dbc.Row([
dbc.Col([
dcc.Graph(id="sales-chart", figure=create_sales_chart())
], width=12, lg=6),
dbc.Col([
dcc.Graph(id="product-chart", figure=create_product_chart())
], width=12, lg=6)
]),
dbc.Row([
dbc.Col([
html.Div(id="summary-stats", className="mt-4 p-3 border rounded")
])
])
], fluid=True)
# 回調函數 - 根據選擇年份更新圖表
@app.callback(
Output("sales-chart", "figure"),
Input("year-dropdown", "value")
)
def update_sales_chart(selected_year):
if selected_year == "all":
return create_sales_chart()
else:
fig = px.line(sales_data, x=sales_data.index, y=selected_year,
title=f"{selected_year} 年銷售趨勢",
labels={"value": "銷售額", "variable": "年份"},
markers=True)
return fig
# 啟動應用
if __name__ == "__main__":
app.run_server(debug=True)
進階 JSON 資料視覺化範例
16. 處理多層次 JSON 資料
# 複雜的巢狀 JSON 結構
complex_data = {
"company": {
"name": "ABC Corp",
"departments": [
{
"name": "Sales",
"regions": [
{"name": "North", "revenue": [1200, 1300, 1250, 1400, 1350, 1500], "employees": 12},
{"name": "South", "revenue": [900, 950, 1000, 1050, 1100, 1150], "employees": 10},
{"name": "East", "revenue": [800, 850, 900, 950, 1000, 1100], "employees": 8},
{"name": "West", "revenue": [1100, 1200, 1250, 1300, 1350, 1400], "employees": 11}
]
},
{
"name": "Marketing",
"regions": [
{"name": "North", "revenue": [500, 550, 600, 650, 700, 750], "employees": 6},
{"name": "South", "revenue": [450, 500, 550, 600, 650, 700], "employees": 5},
{"name": "East", "revenue": [400, 450, 500, 550, 600, 650], "employees": 4},
{"name": "West", "revenue": [550, 600, 650, 700, 750, 800], "employees": 7}
]
}
],
"months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
}
}
# 儲存複雜 JSON 資料
with open('complex_data.json', 'w') as f:
json.dump(complex_data, f)
# 讀取複雜 JSON 資料
with open('complex_data.json', 'r') as f:
complex_data = json.load(f)
# 展平複雜 JSON 結構為 DataFrame
flat_data = []
months = complex_data["company"]["months"]
for dept in complex_data["company"]["departments"]:
dept_name = dept["name"]
for region in dept["regions"]:
region_name = region["name"]
employees = region["employees"]
for month_idx, month in enumerate(months):
revenue = region["revenue"][month_idx]
flat_data.append({
"department": dept_name,
"region": region_name,
"month": month,
"revenue": revenue,
"employees": employees
})
flat_df = pd.DataFrame(flat_data)
# 視覺化複雜資料
fig = px.sunburst(
flat_df,
path=['department', 'region', 'month'],
values='revenue',
color='revenue',
color_continuous_scale='RdBu',
title='部門與區域收入結構'
)
fig.update_layout(
margin=dict(t=60, l=25, r=25, b=25)
)
fig.show()
17. 地理資料視覺化
# 地理資料 JSON
geo_data = {
"cities": [
{"name": "台北", "lat": 25.0330, "lon": 121.5654, "population": 2.6, "events": 120},
{"name": "台中", "lat": 24.1477, "lon": 120.6736, "population": 2.8, "events": 95},
{"name": "高雄", "lat": 22.6273, "lon": 120.3014, "population": 2.7, "events": 100},
{"name": "台南", "lat": 22.9999, "lon": 120.2269, "population": 1.8, "events": 85},
{"name": "新竹", "lat": 24.8138, "lon": 120.9675, "population": 0.7, "events": 70},
{"name": "彰化", "lat": 24.0813, "lon": 120.5439, "population": 1.3, "events": 65}
]
}
# 儲存地理資料
with open('geo_data.json', 'w') as f:
json.dump(geo_data, f)
# 讀取地理資料
with open('geo_data.json', 'r') as f:
geo_data = json.load(f)
# 轉換為 DataFrame
cities_df = pd.DataFrame(geo_data["cities"])
# 繪製地圖
fig = px.scatter_mapbox(
cities_df,
lat="lat",
lon="lon",
hover_name="name",
size="population",
color="events",
color_continuous_scale=px.colors.cyclical.IceFire,
size_max=15,
zoom=7,
title="台灣主要城市分布與活動數",
mapbox_style="carto-positron"
)
fig.update_layout(
margin={"r":0,"t":50,"l":0,"b":0}
)
fig.show()