資料科學 視覺化 圖表 程式設計

Plotly 互動式圖表指南

Plotly 圖表教學指南

Plotly 是一個功能強大的 Python 資料視覺化函式庫,支援互動式與動態圖表,尤其適合用於網頁應用和資料分析。相較於 Matplotlib,Plotly 提供更豐富的互動性和美觀的預設樣式,能夠輕鬆製作出專業級的資料視覺化圖表。
本教學將從基礎到進階,示範如何使用 Plotly 繪製各種類型的圖表,並特別聚焦於如何從 JSON 資料中建立視覺化。

目錄

  1. 基礎設定
  2. 從 JSON 資料入手
  3. 基本圖表類型
  4. 進階圖表
  5. 組合與交互式圖表
  6. 自訂樣式與佈局
  7. 匯出與分享

基礎設定

首先,我們需要安裝並匯入必要的套件:

# 安裝套件
# 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()