Tham gia khóa học Power BI Thực Chiến để giải 100+ Challenges từ căn bản đến nâng cao – Learning by Doing Challenges. Inbox mình Tại Đây.
Challenge
Xây dựng biểu đồ Rolling N-Month Trend với các tính năng:
- Dynamic Period Selection – User chọn được khoảng thời gian: 3, 6, 9, hoặc 12 tháng
- Year-over-Year Comparison – So sánh với cùng kỳ năm trước
- Smart Tooltip – Hiển thị insight chi tiết khi hover vào biểu đồ:
- Doanh thu hiện tại
- So sánh với cùng kỳ năm trước
- Phần trăm tăng/giảm (có màu và mũi tên)
- Top 5 sản phẩm bán chạy nhất trong giai đoạn
Yêu Cầu 1: User có thể lựa chọn xem doanh thu tích lũy của 3, 6, 9, hoặc 12 tháng gần nhất.
- Tạo một Slicer với 4 lựa chọn: “3 Tháng”, “6 Tháng”, “9 Tháng”, “12 Tháng”
- Khi user chọn → Tất cả biểu đồ và KPI tự động cập nhật theo period đã chọn
- Tiêu đề biểu đồ cũng phải tự động đổi
Yêu Cầu 2: Tại mỗi điểm trên biểu đồ, hiển thị tổng doanh thu của N tháng liền kề trước đó.
- “Rolling” nghĩa là “trượt” – mỗi điểm đều cộng dồn N tháng gần nhất
- VD: Rolling 6 Months Sales = Tổng doanh thu từ 04/2025 → 09/2025
Yêu Cầu 3: Tại mỗi điểm, hiển thị thêm đường so sánh với cùng kỳ năm trước để đánh giá tăng trưởng.
- Rolling N Months Sales LY = Lùi 1 năm so với giai đoạn hiện tại
- Hiển thị bằng đường nét đứt trên biểu đồ
- VD:
- Rolling 9M Sales (TY) = 09/2024 → 05/2025
- Rolling 9M Sales (LY) 09/2023 → 05/2024
Yêu Cầu 4: Khi hover vào một điểm trên biểu đồ, hiển thị tooltip với đầy đủ thông tin phân tích.
| Thành phần | Mô tả | Ví dụ |
|---|---|---|
| Tiêu đề | Period + Tháng đang xem | “Rolling 9 Months Sales: 05/2025” |
| Doanh thu chính | 846.54M | |
| vs YOY | So sánh cùng kỳ | 914.38M |
| Growth % | Có mũi tên + màu sắc | ▼ 7.4% (đỏ) |
| Variance | Chênh lệch tuyệt đối | ▼ 67.8M (đỏ) |
| Top 5 Products | Sản phẩm bán chạy nhất | Danh sách 5 sản phẩm |
Yêu Cầu 5: Trong tooltip, hiển thị 5 sản phẩm có doanh thu cao nhất trong giai đoạn Rolling N tháng.
- Cho phép user nhanh chóng biết sản phẩm nào đang “hot” và sản phẩm nào đang sụt giảm trong giai đoạn đang xem.
- Format hiển thị: [Rank]. [Tên SP]: [Doanh thu] ([YoY%])
Yêu Cầu 6: Vẽ biểu đồ xu hướng với 2 đường để dễ so sánh TY vs LY.
- Đường nét liền: Rolling N Months Sales (Năm nay)
- Đường nét đứt: Rolling N Months Sales LY (Cùng kỳ năm trước)
- Đường phải dừng lại tại tháng có dữ liệu cuối cùng (không vẽ ra tương lai)
Yêu Cầu 7: Tiêu đề biểu đồ tự động thay đổi theo context.
- Format: “Rolling [N] Months Sales: [MM/YYYY]”
- VD: Chọn “3 Tháng” → “Rolling 3 Months Sales: 12/2025”
DAX Code Step by Step
Bước 01: User-Defined Functions (UDF)
Định nghĩa UDFs trong DAX Query View:
DEFINE
-- Format số với K/M/B suffix
FUNCTION FormatKMB = (num : NUMERIC) =>
-- Nhận vào 1 giá trị số (Scalar), trả về 1 chuỗi text (Scalar).
SWITCH(
TRUE(),
-- Input 1,500,000,000. ABS >= 1E9 (1 Tỷ) -> True.
-- 1.5 tỷ / 1 tỷ = 1.5. Format chuỗi -> "1.5B"
ABS(num) >= 1E9, FORMAT(num / 1E9, "#,##0.0") & "B",
-- Input -2,500,000. ABS >= 1E6 (1 Triệu) -> True.
-- -2.5 triệu / 1 triệu = -2.5. Format chuỗi -> "-2.5M"
ABS(num) >= 1E6, FORMAT(num / 1E6, "#,##0.0") & "M",
-- Input 15,000. ABS >= 1E3 (1 Nghìn) -> True. Output "15.0K"
ABS(num) >= 1E3, FORMAT(num / 1E3, "#,##0.0") & "K",
-- Input 500. Các điều kiện trên False. Giữ nguyên số. Output "500"
FORMAT(num, "#,##0")
)
-- Lấy mũi tên chỉ hướng
FUNCTION GetArrow = (num : NUMERIC) =>
IF(num >= 0, "▲ ", "▼ ")
-- Format % (Chỉ lấy giá trị tuyệt đối)
FUNCTION FormatPercent = (num : NUMERIC) =>
-- Hàm FORMAT chuyển đổi số sang dạng text theo mask "0.0%".
-- Dùng ABS(num) vì dấu âm/dương sẽ được thể hiện bằng Mũi tên ở hàm khác.
-- Input -0.153 -> ABS(0.153) -> Output "15.3%"
FORMAT(ABS(num), "0.0%")
-- Get Color từ Text (Phân tích chuỗi kết quả để gán màu)
FUNCTION GetColorFromText = (txt : STRING) =>
-- Input là STRING (Kết quả của hàm FormatWithArrow), không phải số.
-- CONTAINSSTRING thực hiện tìm kiếm text (Case-insensitive).
SWITCH(
TRUE(),
-- Input "▲ 15.3%". Tìm thấy "▲" -> True -> Trả về Hex Xanh
CONTAINSSTRING(txt, "▲"), "#0d6a3f", -- Xanh (tăng)
-- Input "▼ 2.1M". Tìm thấy "▼" -> True -> Trả về Hex Đỏ
CONTAINSSTRING(txt, "▼"), "#a1343c", -- Đỏ (giảm)
-- [Default]: Không tìm thấy ký tự đặc biệt -> Trả về màu Xám
"#909090" -- Xám (còn lại)
)
-- Format số lượng lớn kèm mũi tên
FUNCTION FormatWithArrow = (num : NUMERIC) =>
-- Function Chaining (Gọi hàm trong hàm).
-- Toán tử "&" dùng để nối chuỗi (String Concatenation).
-- Input 35,600,000
-- Step 1: GetArrow(35.6M) -> "▲ "
-- Step 2: FormatKMB(35.6M) -> "35.6M" (ABS đã xử lý trong hàm con)
-- Result: "▲ 35.6M"
GetArrow(num) & FormatKMB(ABS(num))
-- Format phần trăm kèm mũi tên
FUNCTION FormatPercentWithArrow = (num : NUMERIC) =>
-- Tương tự trên, tái sử dụng logic đã định nghĩa để đảm bảo nhất quán.
-- Input -0.052
-- Step 1: GetArrow(-0.052) -> "▼ "
-- Step 2: FormatPercent(-0.052) -> "5.2%"
-- Result: "▼ 5.2%"
GetArrow(num) & FormatPercent(num)
Bước 02: Tạo Bảng Tham Số
Rolling Period Table =
DATATABLE(
"Period", INTEGER,
"Label", STRING,
{
{3, "3 Tháng"},
{6, "6 Tháng"},
{9, "9 Tháng"},
{12, "12 Tháng"}
}
)
Bước 03
Selected Period = SELECTEDVALUE('Rolling Period Table'[Period], 12)
Bước 04
Total Sales = SUM(FactSales[SalesAmount])
Bước 05
Rolling N Months Sales =
VAR SelectedPeriod = [Selected Period]
-- Context: User chọn Slicer là "12 Tháng" -> Biến SelectedPeriod lưu giá trị số nguyên: 12.
VAR LastSalesDate =
CALCULATE(
MAX(FactSales[OrderDate]),
REMOVEFILTERS('DimDate')
)
-- MAX: Tìm ngày lớn nhất trong cột OrderDate.
-- REMOVEFILTERS('DimDate'): Chỉ gỡ bỏ bộ lọc từ bảng Date (trục thời gian của biểu đồ),
-- nhưng vẫn GIỮ NGUYÊN bộ lọc từ các bảng khác (VD: Khu vực, Sản phẩm).
-- Context:
-- Cần biết ngày bán hàng cuối cùng là ngày nào để chặn biểu đồ không vẽ ra tương lai.
-- Nếu đang chọn Slicer "Miền Bắc" (dừng bán tháng 6), hàm này sẽ trả về 30/06, thay vì 31/12 của toàn công ty.
-- Giả sử data toàn công ty đến 31/12/2024.
-- Nhưng Slicer chọn "Miền Bắc" (chỉ có data đến 30/06/2024) => Biến LastSalesDate = 30/06/2024.
VAR CurrentDate = MAX('DimDate'[Date])
-- Lấy giá trị lớn nhất của cột Date trong Filter Context hiện tại.
-- Context: Xác định xem điểm dữ liệu đang được tính toán nằm ở tháng nào trên trục X.
-- Tại điểm hiển thị "Tháng 8/2024" trên biểu đồ Line Chart => Biến CurrentDate = 31/08/2024.
RETURN
IF(
CurrentDate <= LastSalesDate,
-- Context: "Tháng đang vẽ trên biểu đồ có nằm trong quá khứ/hiện tại không?"
-- TH1 (Tháng 8): 31/08/2024 <= 30/06/2024 (LastDate miền Bắc)? -> FALSE.
-- TH2 (Tháng 5): 31/05/2024 <= 30/06/2024? -> TRUE.
-- [Nếu TRUE: Tính toán Rolling]
CALCULATE(
[Total Sales], -- Measure gốc cần tính
DATESINPERIOD(
'DimDate'[Date], -- Cột ngày cần dịch chuyển
MAX('DimDate'[Date]), -- Điểm neo: Lấy ngày cuối của tháng hiện tại (CurrentDate)
-SelectedPeriod, -- Khoảng dịch chuyển: Lùi lại 12 (hoặc N) khoảng
MONTH -- Đơn vị: Tháng
)
),
-- DATESINPERIOD trả về một bảng danh sách các ngày liên tục trong khoảng lùi 12 tháng.
-- CALCULATE ép buộc [Total Sales] phải tính tổng trong khoảng thời gian mới này.
-- Context: Cộng dồn doanh thu của 12 tháng gần nhất lại.
-- Tại tháng 05/2024. Neo là 31/05/2024.
-- DATESINPERIOD tạo list ngày từ: 01/06/2023 đến 31/05/2024.
-- CALCULATE tính tổng tiền trong giai đoạn này.
-- [Nếu FALSE: Trả về Rỗng]
BLANK()
-- Context: Ẩn đường vẽ trên biểu đồ.
-- Tại tháng 08/2024 (lớn hơn 30/06/2024), trả về Blank -> Biểu đồ ngắt nét.
)
Bước 06
Rolling N Months Sales LY =
CALCULATE(
[Rolling N Months Sales],
DATEADD('DimDate'[Date], -1, YEAR)
)
Bước 07
Rolling Sales % Growth =
VAR GrowthRate = DIVIDE(
[Rolling N Months Sales] - [Rolling N Months Sales LY],
[Rolling N Months Sales LY],
0
)
RETURN FormatPercentWithArrow(GrowthRate)
Bước 08
Rolling Sales Variance =
VAR _Var = [Rolling N Months Sales] - [Rolling N Months Sales LY]
RETURN FormatWithArrow(_Var)
Bước 09
Get Color = GetColorFromText([Rolling Sales % Growth])
Bước 10
Dynamic Trend Title =
"Rolling " & [Selected Period] & " Months Sales: " & FORMAT(MAX(DimDate[Date]), "MM/yyyy")
Bước 11
Top 5 Products Rolling =
VAR SelectedPeriod = [Selected Period]
VAR CurrentDate = MAX(DimDate[Date])
-- Xác định Filter Context cho trục thời gian
-- Trả về bảng date chứa range ngày từ (CurrentDate - N Month) đến CurrentDate
-- Tại datapoint T12/2024, Period=3 -> Bảng Date chứa 01/10/2024..31/12/2024
VAR PeriodDates = DATESINPERIOD(DimDate[Date], CurrentDate, -SelectedPeriod, MONTH)
-- Chỉ giữ lại 5 dòng có giá trị cao nhất, loại bỏ các dòng không cần thiết
VAR Top5Products =
TOPN(
5,
-- Context Manipulation: Sử dụng CALCULATETABLE để áp dụng PeriodDates Filter vào danh sách sản phẩm
-- Chỉ xếp hạng dựa trên doanh thu của giai đoạn Rolling này
CALCULATETABLE(VALUES(DimProduct[ProductName]), PeriodDates),
[Total Sales],
DESC
)
-- Thực hiện tính toán phức tạp trên tập dữ liệu đã thu gọn
-- ADDCOLUMNS là Iterator, lặp qua 5 dòng kết quả của bước trên
VAR Top5WithDetails =
ADDCOLUMNS(
Top5Products,
-- Context Transition: Row Context (Product Name) chuyển thành Filter Context
-- Kết hợp với PeriodDates Filter để tính Sales TY
"SalesTY", CALCULATE([Total Sales], PeriodDates),
-- Time Intelligence Logic: Dịch chuyển Filter Context của bảng PeriodDates lùi 1 năm
-- Filter Date (2024) -> Filter Date (2023) áp dụng cho cùng sản phẩm đó
"SalesLY", CALCULATE([Total Sales], DATEADD(PeriodDates, -1, YEAR)),
-- Re-ranking: Tính lại Rank chính xác trong phạm vi 5 sản phẩm này
"Rank", RANKX(Top5Products, CALCULATE([Total Sales], PeriodDates), , DESC, DENSE)
)
RETURN
-- Tạo chuỗi kết quả hiển thị
CONCATENATEX(
Top5WithDetails,
VAR GrowthPct = DIVIDE([SalesTY] - [SalesLY], [SalesLY], 0)
VAR Arrow = IF(GrowthPct >= 0, "▲", "▼")
VAR GrowthText = Arrow & FORMAT(ABS(GrowthPct), "0%")
RETURN
-- String Format: Rank. Name: Sales (Growth)
[Rank] & ". " & [ProductName] & ": " & FormatKMB([SalesTY]) & " (" & GrowthText & ")",
-- Delimiter: Ký tự xuống dòng
UNICHAR(10),
-- Sort Order: Sắp xếp dòng theo Rank tăng dần
[Rank], ASC
)

Leave a Comment