Power BI Case Study

Sales Performance Dashboard
2024–2025

Raw multi-store retail sales data, cleaned and standardized with Power Query, modeled as a star schema, and calculated with 13 DAX measures — then presented as a two-page executive dashboard. This page rebuilds that report for the web, using the actual numbers queried from the model.

transaction rows
product SKUs
active stores
data period
13DAX measures
  1. 01ExtractExcel workbook, 4 sheets (lookups & 2 years of transaction data)
  2. 02TransformPower Query — rename, cast types, merge, calculated columns
  3. 03ModelStar schema — 1 fact table, 3 dimension tables
  4. 04Measure13 DAX measures — aggregation, time intelligence, Pareto
  5. 05Visualize2-page executive dashboard

Backend

Data Modelling & Data Engineering

The part a dashboard viewer never sees, but the part that determines whether the numbers can be trusted: the data source, the standardization process, the table relationships, and the DAX logic behind every metric.

1 Source Data & Standardization (Power Query / M)

All data comes from one Excel workbook. The file below has the assignment brief and an unused product sheet stripped out, leaving the four sheets that actually feed the model: STORE LOOKUP, Product Lookup, SALES DATA 2024, and SALES DATA 2025. Each raw sheet has the usual problems of operational data: inconsistent headers, mixed naming conventions, and a schema that changes between years.

Download source data (.xlsx)

STORE LOOKUP — cleaning up column names

Source (raw)Model
sitA_codAsite_code
STORE NAMEStore Name
CityCity Name
LOCATION CATEGORYLocation

The source header even contains a typo (sitA_codA) — cleaned up into a consistent snake_case / Proper Case naming convention across the model.

SALES DATA 2024 vs 2025 — mismatched schemas

The 2025 sheet has an extra pos_number column (POS transaction id) that the 2024 sheet doesn't have — a sign the store's POS system was upgraded partway through the period. Before merging, both sheets were standardized to the same schema:

  • Renamed Store Name/Price/Qty/Revenuestore_name/price/qty/revenue
  • Added a calculated Year = Date.Year([order_date]) column
  • Combined both years into one fact table with Table.Combine()
Source = Table.Combine({#"SALES DATA 2024", #"SALES DATA 2025"})

Because Table.Combine merges by column name, 2024 rows automatically get a blank pos_number — so the Total Transaction measure is most representative for the period since the new POS went live (2025).

2 Model Structure — Star Schema

The Sales Data fact table (126,111 rows) sits at the center, connected to three dimension tables through one-directional many-to-one relationships — a classic star schema that keeps DAX fast and filter context predictable. There's also a _Measures table, deliberately left empty of data columns (a disconnected table), that exists purely as a home for every measure so the field list stays clean and separate from the raw data.

order_date → Date article_code site_code Date Table dimension · custom calendar Sales Data fact · 126,111 rows order_date · site_code · article_code · qty · revenue Product Lookup dimension · 1,507 SKU STORE LOOKUP dimension · 10 stores _Measures 13 DAX measures · no relationship
FromToCardinalityFilter direction
Sales Data[order_date]Date Table[Date]Many → OneSingle
Sales Data[site_code]STORE LOOKUP[site_code]Many → OneSingle
Sales Data[article_code]Product Lookup[article_code]Many → OneSingle

3 Calculated Columns — Enrichment at the Model Layer

Two important columns don't come from the source data — they were added deliberately at the model layer to support sharper business analysis:

Custom Date Table

Built by hand (instead of Power BI's built-in auto date/time) so month sorting and Year/Month stay consistent for time intelligence:

Date Table = CALENDAR(DATE(2024,1,1), DATE(2025,12,31))

Year        = YEAR('Date Table'[Date])
Month Number = MONTH('Date Table'[Date])
Month Name  = FORMAT('Date Table'[Date], "MMMM")

City Category — business segmentation

A business column for comparing store performance inside vs. outside the home city (Depok):

City Category =
IF('STORE LOOKUP'[City Name] = "Depok", "Inside", "Outside")

Used directly in the slicer & the "Revenue per City Category" chart on the dashboard.

Backend

13 DAX Measures

Built in layers: basic aggregation → time intelligence for growth → cumulative ranking for Pareto analysis. All measures live in the disconnected _Measures table.

Frontend

Dashboard — Web Recreation

The first two pages of the original report, rebuilt for the web using the actual numbers queried from the model above (not placeholder data). The filters on the left are interactive — try clicking them.

Total Revenue
Total Quantity
Total Line Items
YTD Growth

Total Revenue per Month, 2024 vs 2025

2025 data available through June 2025

Revenue per Store

Revenue per Brand

Total Revenue
Total Quantity
Total Line Items
YTD Growth

Product Pareto — Revenue Share vs Cumulative %

Top 20 of 1,507 SKUs

Revenue per City Category

Revenue per Channel

Revenue per Location

Revenue per City Name

Revenue per Sales Head