The idea
A DAX measure is one formula, but a report asks it many questions. A matrix with six category rows,
a total and two slicers evaluates Sales seven times, and each time the measure sees different rows
of the data. The set of filters a single evaluation runs under is its filter context.
You never pass the filters in yourself. The cell does it: its row and column headers, every slicer on
the page, the Filters pane and any cross-highlighting all contribute. CALCULATE is the one function
that lets a measure change that set on purpose.
Why it exists
Picture the same report without filter context. You would need a Lighting Sales measure, a Furniture
Sales measure, a 2025 Lighting web Sales measure, and so on for every combination a reader can click.
Spreadsheets work that way: each SUMIFS spells out its own conditions.
Power BI turns it around. You define what a number means once (Sales adds up line_total) and
the report decides which rows it is about. That is why one measure works in a card, a matrix and a
chart without changes, and why slicers "just work".
The cost is that the rows behind a number are invisible. When a figure looks wrong, the formula is rarely the problem. The filter context is, and you can only fix it if you can picture it.
How it works under the hood
Evaluating one cell happens in a fixed order.
- Collect the filters. Everything around the cell adds a filter on one column: the row header
on
products[category], the Year slicer on'Date'[Year], the Channel slicer onorders[channel]. Filters on different columns combine with AND. - Put each filter on its own table, the one that owns the column.
- Let the filters flow along the relationships, from the one side to the many side, until they
reach the table the measure reads. In the Harbor & Pine model every path ends at
order_items. - Evaluate the measure on the rows that survived every filter.
Figure 1
How the filters of one cell reach order_items
'Date'
- Date
- Year
- Month
Year = 2025
orders
- order_id
- order_date
- channel
- status
channel = "web"
status = "completed"
customers
- customer_id
- segment
- state
products
- product_id
- category
category = "Lighting"
order_items
- order_id
- product_id
- line_total
- The Harbor & Pine model. Every line runs from a one side (1) to a many side (*).
order_itemsholds the numbers;Salesadds up itsline_total. - The matrix row for Lighting puts a filter on
products[category]. It travels down the relationship toorder_items: only Lighting lines are left. - The Year slicer filters
'Date'. That filter reachesordersfirst, thenorder_items, two hops. - The Channel slicer lands on
ordersand follows the same path down. All three filters now meet onorder_itemsand combine with AND. - Filters only flow from one to many. The category filter does not climb back up to
orders, soCOUNTROWS(orders)would still count orders of every category. CALCULATE(..., orders[status] = "completed")adds one more filter before the sum runs. It flows down like the others.
Step 3 explains a lot of "wrong" numbers. With single-direction relationships a filter never flows
from many to one. A category filter thins out order_items but leaves orders untouched, which is
why "orders per category" needs DISTINCTCOUNT(order_items[order_id]) and not COUNTROWS(orders).
Here is the same cell as actual rows. The filters remove rows one after another; the sum only runs at the end.
Figure 2
One cell, filter by filter: Lighting, 2025, web
| order_item_id | category | channel | status | line_total |
|---|---|---|---|---|
| 19660 | Lighting | web | completed | 54.99 |
| 19663 | Lighting | web | completed | 189.00 |
| 19749 | Lighting | web | cancelled | 189.00 |
| 19675 | Lighting | app | completed | 219.96 |
| 19679 | Furniture | web | completed | 746.30 |
| 19792 | Textiles | marketplace | completed | 23.99 |
| 19662 | Lighting | web | completed | 31.99 |
| Sales |
|---|
| $275.98 |
- Seven real order lines. Before any filter, a measure would see all of them.
- Row header:
category = "Lighting". Furniture and Textiles drop out. - Channel slicer:
channel = "web". The app line drops out too. Filters on different columns always stack. CALCULATEaddsstatus = "completed". The cancelled line is removed.SUM(order_items[line_total])runs on what is left: 54.99 + 189.00 + 31.99.
What CALCULATE does
CALCULATE(expression, filter1, filter2, ...) runs in three moves:
- Take a copy of the current filter context (the cell's filters).
- Apply each filter argument to the copy. If the column is not filtered yet, the filter is
added. If the column is already filtered, the new filter replaces the old one.
ALL,ALLEXCEPTandREMOVEFILTERSremove filters instead. - Evaluate the expression in the modified copy. The original context is untouched, so the neighbouring cells and the other measures are unaffected.
The "replaces" rule is the one that surprises people, and it is exactly what shares of total rely
on: CALCULATE([Sales], REMOVEFILTERS(products[category])) gives every category row the all-category
total to divide by.
Figure 3
Same matrix, three measures: CALCULATE adds a filter or replaces one
| channel | Gross Sales | Completed Sales | Web Sales |
|---|---|---|---|
| app | $2,262,479 | $2,024,728 | $3,323,593 |
| marketplace | $930,273 | $837,483 | $3,323,593 |
| web | $3,323,593 | $3,032,073 | $3,323,593 |
| Total | $6,516,344 | $5,894,283 | $3,323,593 |
Gross Sales = SUM(order_items[line_total]). Each row filtersorders[channel], so each cell sums that channel's lines.Completed Sales = CALCULATE([Gross Sales], orders[status] = "completed"). Status is a new column, so the filter is added: each channel keeps its row filter and loses its cancelled, refunded and pending lines.Web Sales = CALCULATE([Gross Sales], orders[channel] = "web"). Channel is already filtered by the row, so CALCULATE replaces that filter. Every row shows the web number.- The Total row is the same measures with no channel filter at all, not an addition of the rows. For a plain SUM the two agree; for ratios and distinct counts they don't.
When you want a filter argument to narrow the existing filter rather than overwrite it, wrap it in
KEEPFILTERS. Then the web row of an App Sales measure would be blank, because no order is both web
and app.
Row context is a different thing
A calculated column, or an iterator like SUMX, walks a table one row at a time. That "current row"
is the row context, and it is not a filter. Inside a calculated column on products, the
expression SUM(order_items[line_total]) returns the grand total on every row, because nothing is
filtering order_items: the current product is only a row being visited.
CALCULATE bridges the two. Wrapped in CALCULATE, the current row becomes a filter
(this is called context transition), so CALCULATE(SUM(order_items[line_total])) in that
column gives each product its own sales. A measure reference like [Sales] has an invisible
CALCULATE around it, which is why SUMX(products, [Sales]) works and SUMX(products, SUM(order_items[line_total])) repeats the total.
Seeing the filter context in Power BI Desktop
The practice matrix on this site shows you the filters of each cell. In Power BI Desktop you can read them too: hover a cell for its tooltip, open the Filters pane for visual, page and report filters, and use Optimize → Performance analyzer → Copy query to see the exact filters the visual sends to the model. Format → Edit interactions shows which slicers reach which visuals. When a number surprises you, list its filters there before you touch the formula.
Common misconceptions
"The Total row adds up the rows above it"
The Total row is the measure evaluated again, with the row header's filter missing. For a plain sum that equals the sum of the rows. For an average, a distinct count or a ratio it usually does not: the total of "average order value" is the average over all orders, not the sum of six averages.
"A filter on any table reaches every other table"
Filters move along relationships, one side to many side, and stop there unless a relationship is set
to both directions. A slicer on products never filters customers. If a visual ignores a slicer,
look at the arrows in the model view before you look at the DAX.
"CALCULATE's filter is added to the row's filter"
Only when they are on different columns. On the same column the argument wins and the row's filter is
dropped, which is how "the same number on every row" happens. Use KEEPFILTERS when you mean "and".
Check yourself
One question. Answering it ticks this lesson off on the map; it never locks a level.
Not quite. That would happen if the filters were combined with AND. A filter argument on a column that's already filtered replaces the old filter; KEEPFILTERS is how you'd get the blank.Try another answer.
Right. Same column, so CALCULATE throws away channel = web from the row and puts channel = app in its place. Every row shows the app figure.
Not quite. It's the other way round: CALCULATE runs after the row's filters are collected and can overwrite them. That is what makes it powerful.Try another answer.
Not quite. Filters never add values together. A filter on channel keeps a set of channels; this one keeps only app.Try another answer.
Lesson done: it is ticked on the map.