Python Viz 4 — Plotly: biểu đồ tương tác cho web

13 thg 7, 2026 3 lượt xem
#dashboard
#python
#plotly
#interactive
#web

Nếu Bài 1 — Tổng quan giúp bạn chọn đúng thư viện, và Matplotlib cho bạn kiểm soát từng pixel của một biểu đồ tĩnh, thì Plotly giải quyết một nhu cầu khác hẳn: biểu đồ tương tác chạy trong trình duyệt. Đây là công cụ khi người dùng cuối không phải là bản in PDF gửi Ban điều hành, mà là một trang web nơi lãnh đạo tự rê chuột xem chi tiết, tự phóng to một khoảng thời gian, tự bật/tắt từng chi nhánh để so sánh.

Vì sao cần tương tác

Biểu đồ tĩnh trả lời đúng một câu hỏi mà người vẽ đã định trước. Biểu đồ tương tác cho phép người xem tự đặt câu hỏi tiếp theo mà không cần vẽ lại. Bốn hành vi cốt lõi làm nên khác biệt:

  • Hover — rê chuột lên một điểm/cột hiện ra tooltip đầy đủ số liệu, thay vì phải in nhãn cho mọi điểm (làm rối biểu đồ tĩnh). Với biểu đồ 200 chi nhánh, không ai in 200 nhãn, nhưng hover từng cái thì tự nhiên.
  • Zoom/pan — kéo chọn một vùng để phóng to, xem chi tiết một quý trong chuỗi 3 năm, rồi double-click để về ban đầu. Trên ảnh tĩnh bạn phải vẽ thêm một hình "phóng to".
  • Chọn lọc (selection) — quét chọn một nhóm điểm để làm nổi hoặc lọc; trong dashboard, thao tác này có thể lọc cả các biểu đồ khác (cross-filter).
  • Toggle series — bấm vào một mục trong chú giải (legend) để ẩn/hiện chuỗi đó. Muốn so 2 trong 8 chi nhánh chỉ cần tắt 6 chuỗi còn lại.

Những hành vi này hợp với hai tình huống: khám phá dữ liệu (analyst tự dò tìm bất thường) và dashboard web (nhiều người dùng, mỗi người quan tâm một lát cắt khác nhau). Ngược lại, cho báo cáo in, tài liệu PDF, bài báo khoa học — biểu đồ tĩnh của matplotlib vẫn là lựa chọn đúng vì nó cố định, sắc nét khi in, và không phụ thuộc JavaScript.

Plotly Express vs graph_objects

Plotly có hai lớp API, và hiểu ranh giới giữa chúng là chìa khoá dùng thư viện hiệu quả.

Plotly Express (import plotly.express as px) là API cấp cao, "một dòng ra biểu đồ". Nó nhận thẳng một DataFrame và tên cột, tự lo phối màu, chú giải, nhãn trục. Các hàm thường dùng:

HàmLoại biểu đồDùng khi
px.lineĐườngXu hướng theo thời gian
px.barCộtSo sánh giữa các nhóm
px.scatterPhân tánQuan hệ hai biến
px.histogramHistogramPhân phối một biến
px.boxBoxplotPhân phối + ngoại lệ
px.treemapTreemapCơ cấu phân cấp theo diện tích
px.sunburstSunburstPhân cấp dạng vòng đồng tâm
px.choroplethBản đồ tô màuChỉ số theo vùng địa lý

graph_objects (import plotly.graph_objects as go) là API cấp thấp, nơi bạn dựng figure từ từng "trace" một (go.Bar, go.Scatter, go.Choropleth) và cấu hình layout chi tiết. Bạn cần nó khi Plotly Express không đủ: ghép nhiều loại trace khác nhau trên một hình, trục đôi, subplots không đều, hoặc tinh chỉnh những thuộc tính layout mà Express không phơi ra.

Quy tắc thực dụng: bắt đầu bằng Plotly Express, và khi cần chỉnh sâu thì lấy chính figure Express đó ra chỉnh tiếp — vì px trả về đúng một đối tượng go.Figure. Bạn không phải chọn phe; hai lớp làm việc trên cùng một mô hình dữ liệu.

Figure = data + layout

Mọi biểu đồ Plotly, dù tạo bằng px hay go, đều quy về một cấu trúc duy nhất: một Figure gồm hai phần lớn.

  • data là một danh sách các trace — mỗi trace là một lớp dữ liệu (một chuỗi đường, một nhóm cột, một lớp bản đồ). Trace mang toạ độ, màu, và hovertemplate.
  • layout là mọi thứ bao quanh dữ liệu: tiêu đề, cấu hình trục, chú giải, bảng màu (colorway), chế độ hover (hovermode), và cả frames cho animation.

Khi hình hiển thị, thư viện JavaScript plotly.js đọc cấu trúc này (thực chất là một JSON) và render ra SVG/WebGL trong trình duyệt, đồng thời gắn sẵn mọi hành vi tương tác. Đây là điểm mấu chốt: Plotly Python chỉ sinh ra đặc tả JSON; phần tương tác do plotly.js lo. Vì thế biểu đồ Plotly nhúng vào đâu cũng cần môi trường trình duyệt (hoặc notebook có kernel JS).

Chỉnh figure sau khi tạo là chuyện thường ngày:

fig = px.bar(df, x="chi_nhanh", y="du_no")
fig.update_layout(title="Dư nợ theo chi nhánh", hovermode="x unified")
fig.update_traces(marker_color="#0F4C81")
fig.update_yaxes(tickformat=",.0f", ticksuffix=" tỷ")

Các tính năng tương tác chính

hovertemplate — kiểm soát chính xác nội dung tooltip. Dùng biến %{x}, %{y}, và %{customdata} để định dạng số tiền, phần trăm, thêm dòng:

fig.update_traces(
    hovertemplate="<b>%{x}</b><br>Dư nợ: %{y:,.0f} tỷ<br>Tỷ trọng: %{customdata:.1%}<extra></extra>"
)

<extra></extra> bỏ hộp phụ hiện tên trace. hovermode="x unified" gộp tooltip của mọi chuỗi tại cùng một hoành độ — cực hữu ích khi so nhiều đường theo thời gian.

Legend tương tác có sẵn, không cần cấu hình: bấm một mục để ẩn chuỗi, bấm đúp để chỉ giữ lại chuỗi đó. Đây là lý do một biểu đồ Plotly 8 đường vẫn dùng được, trong khi 8 đường trên matplotlib là "mì spaghetti".

Facet — chia nhỏ theo một biến phân loại thành lưới biểu đồ con, dùng chung trục:

px.line(df, x="thang", y="du_no", color="san_pham",
        facet_col="vung", facet_col_wrap=2)

Animation — thêm chiều thời gian bằng frames. Truyền animation_frame="ky" để Plotly sinh thanh trượt (slider) và nút Play, mỗi frame là một kỳ:

px.scatter(df, x="du_no", y="no_xau_pct", size="tong_ts",
           color="vung", animation_frame="quy", animation_group="chi_nhanh")

Subplots và trục đôi — dùng plotly.subplots.make_subplots khi cần ghép nhiều loại trace hoặc hai thang đo:

from plotly.subplots import make_subplots
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Bar(x=x, y=du_no, name="Dư nợ"), secondary_y=False)
fig.add_trace(go.Scatter(x=x, y=so_ho_so, name="Số hồ sơ"), secondary_y=True)

Bản đồ — Plotly có sẵn biểu đồ địa lý: px.choropleth (tô màu vùng theo chỉ số) và px.scatter_mapbox/px.scatter_geo (điểm trên nền bản đồ). Với dữ liệu chi nhánh theo tỉnh/thành, một choropleth Việt Nam cho thấy phân bố dư nợ trực quan hơn mọi bảng số. Chủ đề bản đồ và hệ toạ độ được bàn kỹ ở Trực quan hoá không gian địa lý.

Xuất và nhúng

Vì Plotly sinh ra đặc tả JSON + dựa vào plotly.js, có nhiều cách "đóng gói" một hình:

  • HTML tương tác: fig.write_html("bao_cao.html") tạo một file HTML độc lập giữ nguyên mọi tương tác. Tuỳ chọn include_plotlyjs: True (nhúng cả ~3MB thư viện, mở offline được), "cdn" (chỉ tham chiếu CDN, file nhẹ nhưng cần mạng), hoặc False khi trang đã có plotly.js sẵn.
  • Nhúng notebook/web: trong Jupyter, gọi fig.show() là đủ. Trong ứng dụng web, fig.to_html(full_html=False) trả về một mảnh <div> để chèn vào template. fig.to_json() cho phép đẩy đặc tả sang frontend tự render.
  • Ảnh tĩnh: fig.write_image("hinh.png") xuất PNG/SVG/PDF, nhưng cần cài kaleido (pip install kaleido) — đây là engine render tĩnh headless. Không có kaleido thì lệnh này lỗi. Dùng khi cần đưa biểu đồ Plotly vào một báo cáo PDF hoặc slide.

Hiệu năng với nhiều điểm

Mặc định Plotly render bằng SVG — đẹp, nhưng chậm và ngốn bộ nhớ khi vượt vài nghìn điểm. Ba hướng xử lý:

  • WebGL: dùng biến thể scattergl thay scatter (px.scatter(..., render_mode="webgl")) để đẩy việc vẽ xuống GPU. Xử lý hàng trăm nghìn điểm mượt hơn hẳn.
  • Lấy mẫu (sampling): với chuỗi thời gian dày đặc, lấy mẫu hoặc downsample trước khi vẽ — mắt người không phân biệt được 2 triệu điểm.
  • Tổng hợp (aggregation): gộp trước bằng SQL/pandas (theo ngày, theo nhóm) rồi mới vẽ. Đây gần như luôn là cách đúng cho dashboard: đẩy phần nặng xuống tầng dữ liệu, gửi lên trình duyệt số điểm vừa phải.

Plotly là nền của Dash

Plotly (thư viện vẽ) và Dash (framework dựng web app) là hai tầng của cùng một hệ sinh thái do Plotly Inc. phát triển. Dash cho phép ghép các biểu đồ Plotly với dropdown, slider, ô nhập rồi nối chúng bằng "callback" (Python) để tạo dashboard tương tác đầy đủ — lọc theo vùng, chọn kỳ, cross-filter giữa các biểu đồ.

So với Streamlit: Dash mạnh về ứng dụng production nhiều trang, kiểm soát layout và luồng dữ liệu chặt (mô hình callback tường minh), nhưng dốc học hơn. Streamlit ưu tiên tốc độ dựng nhanh, script tuyến tính chạy lại từ trên xuống — hợp prototype và công cụ nội bộ. Cả hai đều render biểu đồ Plotly tốt. Chủ đề dựng dashboard bằng Streamlit được bàn ở Streamlit dashboards.

Khi nào Plotly, khi nào Altair hay Matplotlib

Tiêu chíMatplotlibAltairPlotly
Tương tácTĩnh (gần như không)Có, khai báo gọnRất mạnh, đầy đủ
Đích đếnIn/PDF, báo cáoWeb/notebook, khám pháWeb, dashboard, app
Cách viếtMệnh lệnh, chi tiếtKhai báo (grammar)Cấp cao (px) + chi tiết (go)
Nhiều điểmhexbin, samplingGiới hạn ~5000 mặc địnhWebGL rất tốt
Bản đồCần cartopy/geopandasCó, hạn chếChoropleth/mapbox sẵn
  • Matplotlib: khi đích là bản in, PDF chính thức, hoặc cần kiểm soát pixel tuyệt đối.
  • Altair: khi muốn khám phá nhanh trong notebook với cú pháp khai báo ngắn gọn, dữ liệu vừa phải — xem Altair & grammar of graphics.
  • Plotly: khi cần tương tác thật sự trên web, dashboard nhiều người dùng, dữ liệu lớn (WebGL), hoặc bản đồ.

Ví dụ: dư nợ theo chi nhánh theo thời gian (minh hoạ)

Ví dụ dưới minh hoạ một biểu đồ đường tương tác: dư nợ từng chi nhánh theo tháng, hover xem chi tiết, legend bật/tắt chuỗi. Số liệu là giả định.

import plotly.express as px
import pandas as pd

# --- dữ liệu minh hoạ ---
df = pd.DataFrame({
    "thang": ["01","02","03","04","05","06"] * 3,
    "chi_nhanh": (["Hà Nội"]*6 + ["TP.HCM"]*6 + ["Đà Nẵng"]*6),
    "du_no": [4100,4150,4180,4220,4260,4300,
              3700,3720,3760,3780,3810,3850,
              1400,1420,1450,1460,1490,1500],   # tỷ VND
})

NCB = {"Hà Nội": "#0F4C81", "TP.HCM": "#C9A227", "Đà Nẵng": "#3E8E7E"}

fig = px.line(df, x="thang", y="du_no", color="chi_nhanh",
              color_discrete_map=NCB, markers=True,
              labels={"thang": "Tháng", "du_no": "Dư nợ (tỷ VND)",
                      "chi_nhanh": "Chi nhánh"},
              title="Dư nợ theo chi nhánh — 6 tháng đầu 2026")

fig.update_traces(
    hovertemplate="<b>%{fullData.name}</b><br>Tháng %{x}<br>"
                  "Dư nợ: %{y:,.0f} tỷ<extra></extra>")
fig.update_layout(hovermode="x unified",
                  legend_title_text="Chi nhánh (bấm để ẩn/hiện)")
fig.update_yaxes(rangemode="tozero", tickformat=",.0f")

fig.write_html("du_no_chi_nhanh.html", include_plotlyjs="cdn")
# fig.write_image("du_no_chi_nhanh.png")   # cần cài kaleido

rangemode="tozero" giữ trục Y bắt đầu từ 0 (tránh phóng đại chênh lệch — cạm bẫy đã bàn ở Matplotlib). File HTML sinh ra tự có zoom, pan, hover, và legend bật/tắt.

Ví dụ: treemap cơ cấu danh mục (minh hoạ)

Treemap trực quan hoá cơ cấu phân cấp bằng diện tích ô — hợp để thấy nhanh sản phẩm nào chiếm tỷ trọng dư nợ lớn nhất. Số liệu giả định.

df_dm = pd.DataFrame({
    "nhom": ["Bán lẻ","Bán lẻ","Bán lẻ","Doanh nghiệp","Doanh nghiệp"],
    "san_pham": ["Vay mua nhà","Vay tiêu dùng","Thẻ tín dụng",
                 "Vay vốn lưu động","Tài trợ thương mại"],
    "du_no": [5200, 1800, 900, 6100, 2400],   # tỷ VND
})

fig = px.treemap(df_dm, path=["nhom", "san_pham"], values="du_no",
                 color="du_no", color_continuous_scale="Blues",
                 title="Cơ cấu dư nợ theo sản phẩm")
fig.update_traces(
    hovertemplate="<b>%{label}</b><br>Dư nợ: %{value:,.0f} tỷ<br>"
                  "Tỷ trọng: %{percentRoot:.1%}<extra></extra>")
fig.write_html("co_cau_danh_muc.html", include_plotlyjs="cdn")

Rê chuột vào một ô thấy tỷ trọng; bấm vào một nhóm để "khoan xuống" (drill-down) chỉ nhóm đó — tương tác có sẵn, không cần code thêm.

Lấy số liệu tổng hợp từ sandbox

Nguyên tắc hiệu năng: tổng hợp ở tầng dữ liệu rồi mới vẽ. Trong sandbox (PostgreSQL read-only), lấy tổng số dư theo loại tiền để đẩy vào px.bar:

-- ▶ Chạy được
SELECT currency,
       COUNT(*)                        AS so_tai_khoan,
       ROUND(SUM(balance)::numeric, 2) AS tong_so_du
FROM accounts
GROUP BY currency
ORDER BY tong_so_du DESC;

Kéo về bằng pandas.read_sql rồi px.bar(df, x="currency", y="tong_so_du") là ra biểu đồ tương tác.

Use case thực tế

Bối cảnh (số liệu ước lượng, minh hoạ): Ban điều hành NCB muốn một dashboard web thay cho bộ slide dư nợ hàng tháng. Yêu cầu: lãnh đạo tự lọc theo vùng (Bắc/Trung/Nam) và theo kỳ (chọn quý), rê chuột lên một chi nhánh xem chi tiết dư nợ, nợ xấu, số hồ sơ; và so sánh nhanh 2–3 chi nhánh bằng cách tắt các chuỗi còn lại. Bộ slide cũ tĩnh, mỗi lần lãnh đạo hỏi "cho xem riêng miền Trung" là analyst phải vẽ lại.

Giải pháp bằng Plotly:

  1. Tầng dữ liệu tổng hợp trước bằng SQL (dư nợ theo chi nhánh × tháng, đã group), trả về DataFrame vài nghìn dòng — không đẩy giao dịch thô lên trình duyệt.
  2. Vẽ bằng Plotly Express: px.line cho xu hướng dư nợ (facet theo vùng), px.treemap cho cơ cấu sản phẩm, px.choropleth bản đồ dư nợ theo tỉnh. Mỗi hình cấu hình hovertemplate định dạng "tỷ VND" và phần trăm.
  3. Ghép vào Dash: dropdown chọn vùng + slider chọn quý, callback lọc DataFrame và cập nhật cả ba biểu đồ (cross-filter). Legend tương tác lo phần so sánh chi nhánh mà không cần thêm UI.
  4. Với biểu đồ phân tán ~150 chi nhánh × 36 tháng, bật render_mode="webgl" để mượt.

Kết quả ước lượng: thay vì mỗi câu hỏi đặc biệt tốn nửa ngày vẽ lại, lãnh đạo tự trả lời được ngay trên dashboard; analyst chỉ bảo trì một app thay vì dựng slide hàng tháng. Bản HTML tương tác (write_html) cũng gửi được cho chi nhánh xem offline khi cần, còn khi phải đưa vào biên bản họp thì write_image (kaleido) xuất PNG tĩnh cho từng biểu đồ. Nguyên tắc thiết kế bố cục và cross-filter được bàn thêm ở Thiết kế dashboard.

Ghi nhớ

  • Plotly giải nhu cầu tương tác web (hover, zoom/pan, chọn lọc, toggle series) — hợp khám phá dữ liệu và dashboard, khác biểu đồ tĩnh của Matplotlib dành cho bản in.
  • Hai lớp API: Plotly Express (px, một dòng ra biểu đồ từ DataFrame) và graph_objects (go, kiểm soát chi tiết); px trả về đúng một go.Figure nên chỉnh tiếp dễ dàng.
  • Mọi figure = data (danh sách trace) + layout (khung hình); plotly.js render và gắn tương tác trong trình duyệt.
  • Tính năng chính: hovertemplate, legend bật/tắt, facet, animation_frame, make_subplots/trục đôi, bản đồ choropleth/scatter_mapbox (xem Geospatial).
  • Xuất: write_html (HTML tương tác, include_plotlyjs="cdn" để nhẹ), nhúng to_html(full_html=False), và ảnh tĩnh cần kaleido.
  • Hiệu năng nhiều điểm: dùng WebGL/scattergl, lấy mẫu, và tổng hợp trước ở tầng dữ liệu.
  • Plotly là nền của Dash (app production, callback tường minh); so với Streamlit (dựng nhanh, script tuyến tính — xem Streamlit).
  • Chọn công cụ: Matplotlib cho in/PDF, Altair cho khám phá khai báo (Altair), Plotly cho web/dashboard/bản đồ/dữ liệu lớn.

Nguồn tham khảo

Bài viết liên quan

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 6

Học cách tổ chức code Python: định nghĩa hàm với tham số vị trí/từ khoá/mặc định, *args/**kwargs, lambda và hàm bậc cao, closure, decorator, generator với yield. Đóng gói code thành module và package, cô lập thư viện bằng môi trường ảo venv, quản lý phụ thuộc với pip và requirements.txt để dự án tái lập được trên mọi máy.

13 thg 7, 2026 5

Biến script thành pipeline đáng tin cậy: cấu trúc project & packaging (uv/poetry), type hints & pydantic, kiểm thử với pytest, logging & cấu hình, đóng gói Docker, và tích hợp CI cho code dữ liệu.

13 thg 7, 2026 5

Hướng dẫn OOP trong Python từ class/instance, kế thừa và super(), đa hình & duck typing, encapsulation tới dunder methods, @property, classmethod/staticmethod, dataclass và type hints (mypy). Kèm nguyên tắc clean code: đặt tên rõ nghĩa, hàm nhỏ, DRY, SOLID cùng chuẩn PEP8 với công cụ ruff/black.

13 thg 7, 2026 5

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!