Phụ lục bài kỹ thuật tương tác dữ liệu trong trực quan hóa

Đây là bài phụ lục minh họa cho bài kỹ thuật tương tác dữ liệu trong trực quan hóa. Bao gồm code, hình minh họa các kỹ thuật.

Dữ liệu bán hàng

Bộ dữ liệu doanh số bán hàng sau sẽ được dùng làm dữ liệu đầu vào cho các ví dụ , hình minh họa trong bài

ThángKhu vựcDanh mụcDoanh thu (triệu)Số đơn hàngGiá bán trung bình (nghìn)Lợi nhuận (triệu)
1BắcĐiện tử12040300024
1TrungThời trang8050160012
1NamGia dụng10045220018
2BắcĐiện tử13542320028
2TrungThời trang8552165013
2NamGia dụng11047230020
3BắcĐiện tử15046330032
3TrungThời trang9555170015
3NamGia dụng12050240022
4BắcĐiện tử16048335034
4TrungThời trang10058172016
4NamGia dụng13052245024
5BắcĐiện tử17050340036
5TrungThời trang11060175018
5NamGia dụng14055250026
6BắcĐiện tử18054345039
6TrungThời trang12064178020
6NamGia dụng15058255028

Kỹ thuật multiple Views

Multiple Views là kỹ thuật vẽ nhiều loại biểu đồ cạnh nhau, mỗi biểu đồ trả lời 1 câu hỏi về bộ dữ liệu. Khi đặt cạnh nhau, chúng giúp người học nhìn cùng một hiện tượng từ nhiều góc độ.

  • Line chart: xu hướng doanh thu theo tháng.
  • Bar chart: so sánh doanh thu theo khu vực.
  • Scatter plot: mối quan hệ giữa giá bán và lợi nhuận.
  • Pie chart: cơ cấu doanh thu theo danh mục.

Overview + Detail

Kỹ thuật Overview + Detail là cách cho người xem thấy cả cái nhìn tổng thể lẫn phần dữ liệu cần quan sát kỹ hơn.

  • Overview: hiển thị toàn bộ dữ liệu hoặc toàn bộ bối cảnh.
  • Detail: làm rõ vùng dữ liệu được chọn hoặc phần cần phân tích sâu.

Lợi ích của cách này là người dùng không bị “lạc” trong dữ liệu lớn. Có thể tập trung vào một vùng cụ thể, và vẫn thấy tổng thể.

Biểu đồ phía trên cho thấy xu hướng của toàn năm, còn biểu đồ phía dưới phóng to giai đoạn biến động mạnh vào cuối tháng 11 và đầu tháng 12. Nhờ đó, người dùng vừa giữ được bức tranh tổng thể, vừa quan sát rõ phần dữ liệu cần phân tích chi tiết.

Small Multiples

Một kỹ thuật khác là Small Multiples, còn gọi là Trellis. Thay vì tạo một biểu đồ lớn chứa tất cả thông tin, ta tách dữ liệu thành nhiều nhóm nhỏ và hiển thị thành nhiều biểu đồ giống nhau, đặt song song theo lưới.

Với bộ dữ liệu doanh số bán hàng ở trên. Cùng một biến doanh thu theo tháng được chia thành nhiều biểu đồ đường nhỏ theo từng khu vực. Nhờ giữ nguyên loại biểu đồ, trục thời gian và thang đo, người đọc có thể dễ dàng so sánh xu hướng doanh thu giữa các khu vực mà không bị rối như khi chồng nhiều đường lên cùng một biểu đồ.

Brushing & Linking – Kết nối các góc nhìn dữ liệu

Đây là một kỹ thuật gồm hai thành phần = Brushing, và Linking. Nếu Multiple Views trả lời câu hỏi “làm sao nhìn dữ liệu từ nhiều góc độ?”, thì Brushing & Linking trả lời câu hỏi “làm sao để các góc nhìn đó tương tác với nhau?”.

  • Hình bên trái: trạng thái ban đầu, scatter plot, bar chart và bảng dữ liệu hiển thị toàn bộ dữ liệu.
  • Hình bên phải: sau khi người dùng brushing trên scatter plot, bar chart và bảng dữ liệu cập nhật theo vùng được chọn.

Người dùng khoanh chọn một nhóm điểm trên scatter plot thể hiện mối quan hệ giữa giá bán trung bình và lợi nhuận. Sau thao tác chọn này, bar chart doanh thu theo danh mục và bảng dữ liệu bên dưới được cập nhật theo đúng nhóm điểm đã chọn. Nhờ đó, các góc nhìn dữ liệu không còn tồn tại riêng lẻ mà được liên kết với nhau trong cùng một quá trình phân tích.

Code ở trên với plotly như sau

# !pip -q install dash plotly pandas
import pandas as pd
from dash import Dash, dcc, html, dash_table, Input, Output
import plotly.express as px
# 1. Tạo bộ dữ liệu mẫu
data = [
    [1, "Bắc", "Điện tử", 120, 40, 3000, 24],
    [1, "Trung", "Thời trang", 80, 50, 1600, 12],
    [1, "Nam", "Gia dụng", 100, 45, 2200, 18],
    [2, "Bắc", "Điện tử", 135, 42, 3200, 28],
    [2, "Trung", "Thời trang", 85, 52, 1650, 13],
    [2, "Nam", "Gia dụng", 110, 47, 2300, 20],
    [3, "Bắc", "Điện tử", 150, 46, 3300, 32],
    [3, "Trung", "Thời trang", 95, 55, 1700, 15],
    [3, "Nam", "Gia dụng", 120, 50, 2400, 22],
    [4, "Bắc", "Điện tử", 160, 48, 3350, 34],
    [4, "Trung", "Thời trang", 100, 58, 1720, 16],
    [4, "Nam", "Gia dụng", 130, 52, 2450, 24],
    [5, "Bắc", "Điện tử", 170, 50, 3400, 36],
    [5, "Trung", "Thời trang", 110, 60, 1750, 18],
    [5, "Nam", "Gia dụng", 140, 55, 2500, 26],
    [6, "Bắc", "Điện tử", 180, 54, 3450, 39],
    [6, "Trung", "Thời trang", 120, 64, 1780, 20],
    [6, "Nam", "Gia dụng", 150, 58, 2550, 28],
]

columns = [
    "Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)",
    "Số đơn hàng", "Giá bán TB (nghìn)", "Lợi nhuận (triệu)"
]
df = pd.DataFrame(data, columns=columns)

# 2. Figure ban đầu
scatter_fig = px.scatter(
    df,
    x="Giá bán TB (nghìn)", y="Lợi nhuận (triệu)",
    color="Danh mục",
    symbol="Khu vực",
    hover_data=["Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)"],
    title="Brushing: Chọn điểm trên scatter plot",
)

# Bật box select + lasso select trên thanh công cụ
scatter_fig.update_layout(
    dragmode="select",
    margin=dict(l=40, r=20, t=50, b=40),
)

bar_init = (
    df.groupby("Danh mục", as_index=False)["Doanh thu (triệu)"]
      .sum()
)

bar_fig = px.bar(
    bar_init,
    x="Danh mục", y="Doanh thu (triệu)",
    title="Linking: Doanh thu theo danh mục",
    text_auto=True
)
bar_fig.update_layout(margin=dict(l=40, r=20, t=50, b=40))

# 3. Dash app
app = Dash(__name__)

app.layout = html.Div(
    style={"fontFamily": "Arial", "padding": "20px"},
    children=[
        html.H2("Minh họa Brushing & Linking với Plotly/Dash"),
        html.P(
            "Hãy dùng Box Select hoặc Lasso Select trên scatter plot. "
            "Bar chart và bảng dữ liệu sẽ cập nhật theo vùng bạn chọn."
        ),

        html.Div(
            style={"display": "grid", "gridTemplateColumns": "1.4fr 1fr", "gap": "20px"},
            children=[
                dcc.Graph(
                    id="scatter-graph",
                    figure=scatter_fig,
                    config={"displayModeBar": True}
                ),
                dcc.Graph(
                    id="bar-graph",
                    figure=bar_fig
                ),
            ]
        ),

        html.H4("Bảng dữ liệu sau khi lọc"),
        dash_table.DataTable(
            id="data-table",
            columns=[{"name": c, "id": c} for c in df.columns],
            data=df.to_dict("records"),
            page_size=10,
            style_table={"overflowX": "auto"},
            style_cell={"textAlign": "center", "padding": "6px"},
            style_header={"fontWeight": "bold"},
        )
    ]
)

# 4. Callback cập nhật bar chart + bảng
@app.callback(
    Output("bar-graph", "figure"),
    Output("data-table", "data"),
    Input("scatter-graph", "selectedData"),
)
def update_linked_views(selectedData):
    # Nếu chưa chọn gì, dùng toàn bộ dữ liệu
    if not selectedData or "points" not in selectedData:
        filtered_df = df.copy()
    else:
        # Lấy index từ customdata/pointIndex
        point_indices = [p["pointIndex"] for p in selectedData["points"]]
        filtered_df = df.iloc[point_indices].copy()

    # Bar chart cập nhật theo dữ liệu đã chọn
    bar_data = (
        filtered_df.groupby("Danh mục", as_index=False)["Doanh thu (triệu)"]
        .sum()
    )

    # Đảm bảo luôn hiện đủ 3 danh mục
    all_categories = pd.DataFrame({"Danh mục": ["Điện tử", "Thời trang", "Gia dụng"]})
    bar_data = all_categories.merge(bar_data, on="Danh mục", how="left").fillna(0)

    title = "Linking: Doanh thu theo danh mục"
    if selectedData and "points" in selectedData:
        title += " (sau khi chọn)"

    new_bar_fig = px.bar(
        bar_data,
        x="Danh mục",
        y="Doanh thu (triệu)",
        title=title,
        text_auto=True
    )
    new_bar_fig.update_layout(margin=dict(l=40, r=20, t=50, b=40))

    return new_bar_fig, filtered_df.to_dict("records")

# 5. Chạy app trong notebook/Colab
app.run(jupyter_mode="inline", debug=False)

Brushing & Linking trong Tableau

Trong Tableau, người dùng có thể dùng highlight action, filter action hay dashboard action để tạo hiệu ứng linked views.

Brushing & Linking trong Power BI

Trong Power BI, việc chọn một cột hoặc một vùng dữ liệu ở visual này có thể làm các visual khác highlight hoặc filter theo.

Brushing & Linking trong D3

Trong D3.js, lập trình viên có thể tự xây cơ chế brushing, lasso selection, linked charts rất linh hoạt.

Với D3.js, lập trình viên có thể tự xây dựng cơ chế chọn vùng dữ liệu trên scatter plot bằng brushing hoặc lasso selection, sau đó liên kết lựa chọn đó với các biểu đồ và bảng dữ liệu khác. Điều này giúp tạo các dashboard có mức độ tùy biến và linh hoạt rất cao.

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, Rectangle
# Dữ liệu doanh thu bán hàng
data = [
    [1, "Bắc", "Điện tử", 120, 40, 3000, 24],
    [1, "Trung", "Thời trang", 80, 50, 1600, 12],
    [1, "Nam", "Gia dụng", 100, 45, 2200, 18],
    [2, "Bắc", "Điện tử", 135, 42, 3200, 28],
    [2, "Trung", "Thời trang", 85, 52, 1650, 13],
    [2, "Nam", "Gia dụng", 110, 47, 2300, 20],
    [3, "Bắc", "Điện tử", 150, 46, 3300, 32],
    [3, "Trung", "Thời trang", 95, 55, 1700, 15],
    [3, "Nam", "Gia dụng", 120, 50, 2400, 22],
    [4, "Bắc", "Điện tử", 160, 48, 3350, 34],
    [4, "Trung", "Thời trang", 100, 58, 1720, 16],
    [4, "Nam", "Gia dụng", 130, 52, 2450, 24],
    [5, "Bắc", "Điện tử", 170, 50, 3400, 36],
    [5, "Trung", "Thời trang", 110, 60, 1750, 18],
    [5, "Nam", "Gia dụng", 140, 55, 2500, 26],
    [6, "Bắc", "Điện tử", 180, 54, 3450, 39],
    [6, "Trung", "Thời trang", 120, 64, 1780, 20],
    [6, "Nam", "Gia dụng", 150, 58, 2550, 28],
]
columns = [
    "Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)",
    "Số đơn hàng", "Giá bán TB (nghìn)", "Lợi nhuận (triệu)"
]
df = pd.DataFrame(data, columns=columns)
monthly = df.groupby("Tháng", as_index=False)["Doanh thu (triệu)"].sum()
region = df.groupby("Khu vực", as_index=False)["Doanh thu (triệu)"].sum()
region = region.set_index("Khu vực").loc[["Bắc", "Trung", "Nam"]].reset_index()

# vùng được chọn trên scatter plot
x_min, x_max = 2300, 3500
y_min, y_max = 22, 40
selected = df[
    (df["Giá bán TB (nghìn)"] >= x_min) &
    (df["Giá bán TB (nghìn)"] <= x_max) &
    (df["Lợi nhuận (triệu)"] >= y_min) &
    (df["Lợi nhuận (triệu)"] <= y_max)
].copy()
not_selected = df.drop(selected.index)

after_bar = (
    selected.groupby("Danh mục", as_index=False)["Doanh thu (triệu)"]
    .sum()
    .set_index("Danh mục")
    .reindex(["Điện tử", "Thời trang", "Gia dụng"])
    .fillna(0)
    .reset_index()
)

fig = plt.figure(figsize=(16, 9), dpi=180)
fig.patch.set_facecolor("#f6f8fb")

# Thanh trình duyệt giả lập
ax_browser = fig.add_axes([0.02, 0.93, 0.96, 0.06])
ax_browser.axis("off")
browser = FancyBboxPatch(
    (0, 0), 1, 1,
    boxstyle="round,pad=0.005,rounding_size=0.02",
    linewidth=1, edgecolor="#d7dce3", facecolor="white"
)
ax_browser.add_patch(browser)
ax_browser.text(0.02, 0.5, "●  ●  ●", va="center", ha="left", fontsize=11, color="#9aa4b2")
ax_browser.text(0.12, 0.5, "localhost:8080/d3-sales-dashboard", va="center", ha="left",
                fontsize=12, color="#334155")
ax_browser.text(0.94, 0.5, "D3.js", va="center", ha="right", fontsize=12,
                color="#0f172a", fontweight="bold")

fig.text(0.5, 0.885, "DASHBOARD DOANH THU BÁN HÀNG", ha="center",
         fontsize=21, fontweight="bold", color="#111827")
fig.text(0.5, 0.855,
         "Mô phỏng giao diện D3.js với brushing, lasso selection và linked charts",
         ha="center", fontsize=11, color="#64748b")

def card_axes(rect):
    ax = fig.add_axes(rect)
    ax.set_facecolor("white")
    for spine in ax.spines.values():
        spine.set_visible(False)
    patch = FancyBboxPatch(
        (0, 0), 1, 1,
        boxstyle="round,pad=0.01,rounding_size=0.03",
        transform=ax.transAxes,
        linewidth=1,
        edgecolor="#dbe2ea",
        facecolor="white",
        zorder=-10,
        clip_on=False
    )
    ax.add_patch(patch)
    return ax

# Scatter brushing
ax1 = card_axes([0.04, 0.49, 0.42, 0.30])
ax1.scatter(not_selected["Giá bán TB (nghìn)"], not_selected["Lợi nhuận (triệu)"], alpha=0.28, s=42)
ax1.scatter(selected["Giá bán TB (nghìn)"], selected["Lợi nhuận (triệu)"], s=52)
rect = Rectangle((x_min, y_min), x_max - x_min, y_max - y_min,
                 fill=False, linewidth=2, linestyle="--")
ax1.add_patch(rect)
ax1.set_title("Biểu đồ scatter: chọn vùng dữ liệu bằng brushing / lasso",
              fontsize=12.5, fontweight="bold", pad=12)
ax1.set_xlabel("Giá bán trung bình (nghìn)")
ax1.set_ylabel("Lợi nhuận (triệu)")
ax1.grid(True, alpha=0.22)

# Linked bar chart
ax2 = card_axes([0.50, 0.49, 0.22, 0.30])
ax2.bar(after_bar["Danh mục"], after_bar["Doanh thu (triệu)"])
ax2.set_title("Linked bar chart:\nDoanh thu theo danh mục sau khi chọn",
              fontsize=12, fontweight="bold", pad=12)
ax2.set_ylabel("Doanh thu (triệu)")
ax2.grid(True, axis="y", alpha=0.22)
for i, v in enumerate(after_bar["Doanh thu (triệu)"]):
    ax2.text(i, v + 10, f"{int(v)}", ha="center", va="bottom", fontsize=9)

# Doanh thu theo tháng
ax3 = card_axes([0.75, 0.49, 0.21, 0.30])
ax3.bar(monthly["Tháng"], monthly["Doanh thu (triệu)"])
ax3.set_title("Doanh thu theo tháng", fontsize=12.5, fontweight="bold", pad=12)
ax3.set_xlabel("Tháng")
ax3.set_ylabel("Doanh thu")
ax3.set_xticks(monthly["Tháng"])
ax3.grid(True, axis="y", alpha=0.22)

# Doanh thu theo khu vực
ax4 = card_axes([0.04, 0.11, 0.28, 0.28])
ax4.bar(region["Khu vực"], region["Doanh thu (triệu)"])
ax4.set_title("Doanh thu theo khu vực", fontsize=12.5, fontweight="bold", pad=12)
ax4.set_ylabel("Doanh thu (triệu)")
ax4.grid(True, axis="y", alpha=0.22)

# Bảng dữ liệu
ax5 = card_axes([0.35, 0.11, 0.40, 0.28])
ax5.axis("off")
ax5.text(0.03, 0.93, "Bảng dữ liệu sau khi chọn", fontsize=12.5,
         fontweight="bold", color="#111827")
tbl_df = selected[["Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)", "Giá bán TB (nghìn)", "Lợi nhuận (triệu)"]]
tbl = ax5.table(
    cellText=tbl_df.values,
    colLabels=tbl_df.columns,
    cellLoc="center",
    colLoc="center",
    bbox=[0.02, 0.05, 0.96, 0.80]
)
tbl.auto_set_font_size(False)
tbl.set_fontsize(8.5)
for (r, c), cell in tbl.get_celld().items():
    cell.set_edgecolor("#e5e7eb")
    if r == 0:
        cell.set_text_props(fontweight="bold")

# Khối giải thích tương tác
ax6 = card_axes([0.78, 0.11, 0.18, 0.28])
ax6.axis("off")
ax6.text(0.05, 0.90, "Tương tác kiểu D3.js", fontsize=12.5, fontweight="bold", color="#111827")
items = [
    "• Drag to brush",
    "• Lasso selection",
    "• Linked charts",
    "• Update table",
    "• Custom event handlers"
]
y = 0.76
for item in items:
    ax6.text(0.07, y, item, fontsize=10.2, color="#334155")
    y -= 0.11
ax6.text(0.05, 0.20,
         "D3.js cho phép lập trình viên\nxây dựng cơ chế tương tác\nrất linh hoạt bằng mã JavaScript.",
         fontsize=9.8, color="#64748b")

fig.text(
    0.5, 0.045,
    "Minh họa giao diện kiểu D3.js: người dùng chọn một vùng điểm trên scatter plot, "
    "sau đó biểu đồ cột và bảng dữ liệu được liên kết cập nhật theo vùng đã chọn.",
    ha="center", fontsize=10, color="#475569"
)

output_path = "/mnt/data/d3js_mockup_dung_du_lieu_doanh_thu.png"
plt.savefig(output_path, bbox_inches="tight")
plt.show()
print(output_path)

Brushing & Linking trong Plotly/Dash

Trong Plotly/Dash, dữ liệu lựa chọn từ biểu đồ có thể được truyền qua callback để cập nhật biểu đồ khác.

Code demo vẽ biểu đồ với Plotly trong Google Colab

import pandas as pd
import plotly.express as px
# Bộ dữ liệu doanh thu bán hàng
data = [
    [1, "Bắc", "Điện tử", 120, 40, 3000, 24],
    [1, "Trung", "Thời trang", 80, 50, 1600, 12],
    [1, "Nam", "Gia dụng", 100, 45, 2200, 18],
    [2, "Bắc", "Điện tử", 135, 42, 3200, 28],
    [2, "Trung", "Thời trang", 85, 52, 1650, 13],
    [2, "Nam", "Gia dụng", 110, 47, 2300, 20],
    [3, "Bắc", "Điện tử", 150, 46, 3300, 32],
    [3, "Trung", "Thời trang", 95, 55, 1700, 15],
    [3, "Nam", "Gia dụng", 120, 50, 2400, 22],
    [4, "Bắc", "Điện tử", 160, 48, 3350, 34],
    [4, "Trung", "Thời trang", 100, 58, 1720, 16],
    [4, "Nam", "Gia dụng", 130, 52, 2450, 24],
    [5, "Bắc", "Điện tử", 170, 50, 3400, 36],
    [5, "Trung", "Thời trang", 110, 60, 1750, 18],
    [5, "Nam", "Gia dụng", 140, 55, 2500, 26],
    [6, "Bắc", "Điện tử", 180, 54, 3450, 39],
    [6, "Trung", "Thời trang", 120, 64, 1780, 20],
    [6, "Nam", "Gia dụng", 150, 58, 2550, 28],
]
columns = [
    "Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)",
    "Số đơn hàng", "Giá bán TB (nghìn)", "Lợi nhuận (triệu)"
]
df = pd.DataFrame(data, columns=columns)

# Tổng doanh thu theo danh mục
category_revenue = (
    df.groupby("Danh mục", as_index=False)["Doanh thu (triệu)"]
      .sum()
)
# Vẽ bar chart
fig = px.bar(
    category_revenue,
    x="Danh mục", y="Doanh thu (triệu)",
    text="Doanh thu (triệu)",
    title="Doanh thu theo danh mục"
)

fig.update_traces(
    textposition="outside", width=0.25,
    #marker_color="steelblue", 
    marker_color=["#1f77b4", "#ff7f0e", "#2ca02c"]
)
# textposition="outside" để hiển thị giá trị trên đỉnh cột , outside/inside/auto/none
# width=0.25 chỉnh độ rộng cột bằng 1/4 so với khoảng cách giữa các cột

fig.update_layout(
    xaxis_title="Danh mục", yaxis_title="Doanh thu (triệu)",
    template="seaborn",
    title_xanchor="center", title_x=0.5,
)
#title = 0 - mép trái, 0.5 giữa, 1 = mép phải
#title_xanchor="center" lấy tâm của tiêu đề đặt đúng vào vị trí x
# template="seaborn" màu nền, plotly_white/plotly/plotly_white, plotly_dark/gridon/none

fig.show()

Code demo vẽ biểu đồ với Plotly trong local

  • Tạo folder testplotly
  • Cài thư viện: pip install pandas plotly
  • Tạo file plotly1.py và code
import pandas as pd
import plotly.express as px
df = pd.DataFrame({
    "Danh mục": ["Điện tử", "Thời trang", "Gia dụng"],
    "Doanh thu (triệu)": [915, 590, 750]
})

fig = px.bar(
    df,
    x="Danh mục", y="Doanh thu (triệu)",
    text="Doanh thu (triệu)", 
    title="Doanh thu theo danh mục"
)
fig.update_traces(
    textposition="outside", width=0.25,
    marker_color=["#1f77b4", "#ff7f0e", "#2ca02c"]
)
# textposition="outside" để hiển thị giá trị trên đỉnh cột , outside/inside/auto/none
# width=0.25 chỉnh độ rộng cột bằng 1/4 so với khoảng cách giữa các cột

fig.update_layout(
    xaxis_title="Danh mục", yaxis_title="Doanh thu (triệu)",
    template="seaborn",
    title_xanchor="center", title_x=0.5,
)
fig.show()
  • Chạy file: python plotly1.py

Code demo vẽ biểu đồ với Dash

  • Tạo folder testplotly
  • Cài thư viện: pip install dash pandas plotly
  • Tạo file plotly2.py và code
import pandas as pd
import plotly.express as px
from dash import Dash, dcc, html

# Bộ dữ liệu doanh thu bán hàng
data = [
    [1, "Bắc", "Điện tử", 120, 40, 3000, 24],
    [1, "Trung", "Thời trang", 80, 50, 1600, 12],
    [1, "Nam", "Gia dụng", 100, 45, 2200, 18],
    [2, "Bắc", "Điện tử", 135, 42, 3200, 28],
    [2, "Trung", "Thời trang", 85, 52, 1650, 13],
    [2, "Nam", "Gia dụng", 110, 47, 2300, 20],
    [3, "Bắc", "Điện tử", 150, 46, 3300, 32],
    [3, "Trung", "Thời trang", 95, 55, 1700, 15],
    [3, "Nam", "Gia dụng", 120, 50, 2400, 22],
    [4, "Bắc", "Điện tử", 160, 48, 3350, 34],
    [4, "Trung", "Thời trang", 100, 58, 1720, 16],
    [4, "Nam", "Gia dụng", 130, 52, 2450, 24],
    [5, "Bắc", "Điện tử", 170, 50, 3400, 36],
    [5, "Trung", "Thời trang", 110, 60, 1750, 18],
    [5, "Nam", "Gia dụng", 140, 55, 2500, 26],
    [6, "Bắc", "Điện tử", 180, 54, 3450, 39],
    [6, "Trung", "Thời trang", 120, 64, 1780, 20],
    [6, "Nam", "Gia dụng", 150, 58, 2550, 28],
]
columns = [
    "Tháng", "Khu vực", "Danh mục", "Doanh thu (triệu)",
    "Số đơn hàng", "Giá bán TB (nghìn)", "Lợi nhuận (triệu)"
]
df = pd.DataFrame(data, columns=columns)

# Tổng doanh thu theo danh mục
category_revenue = (
    df.groupby("Danh mục", as_index=False)["Doanh thu (triệu)"]
      .sum()
)

# Vẽ biểu đồ Plotly
fig = px.bar(
    category_revenue,
    x="Danh mục", y="Doanh thu (triệu)",
    text="Doanh thu (triệu)",
    title="Doanh thu theo danh mục"
)

fig.update_traces(
    textposition="outside", width=0.25,
    marker_color=["#1f77b4", "#ff7f0e", "#2ca02c"]
)

fig.update_layout(
    title_x=0.5, title_xanchor="center",
    xaxis_title="Danh mục", yaxis_title="Doanh thu (triệu)",
    template="plotly_white"
)

# Tạo app Dash
app = Dash(__name__)
app.layout = html.Div([
    html.H2("Bar chart trong Dash"),
    dcc.Graph(
        id="bar-chart",
        figure=fig
    )
])

# Chạy trong Colab/Jupyter
app.run(jupyter_mode="inline", debug=False)
  • Chạy file: python plot2.py

Kết quả