06 · Calculation Groups¶
Level 2 wrote Amount YTD, Amount PY, Amount YoY %. Now imagine ten base measures —
Sales, Cost, Margin, Units, Orders… — each needing YTD, PY, YoY, and rolling 3M. That's forty
nearly identical measures, and forty places to fix a bug. A calculation group defines the
time logic once and applies it to whichever measure is in the visual.
The idea¶
A calculation group is a special table with one column. Each row is a calculation item
containing a DAX expression that refers to SELECTEDMEASURE() — "whatever measure is being
evaluated." Put the calculation group's column on a visual (or in a slicer) and each item
transforms the measure.
Creating one¶
Power BI Desktop supports authoring calculation groups directly in Model view (newer releases: Home → Calculation group, or right-click in the Data pane in Model view). Tabular Editor can also create them. When you create the first one, Desktop turns on Discourage implicit measures, because calculation groups only apply to explicit measures.
Step by step: a time-intelligence group¶
Using the Level 2 model with the marked Date table (sales Jan–Mar 2024 380/400/290 and
Jan–Mar 2025 420/460/335):
- Create a calculation group named
Time Calc; rename its column toPeriod. - Add calculation items:
-- Current
SELECTEDMEASURE ()
-- YTD
CALCULATE ( SELECTEDMEASURE (), DATESYTD ( 'Date'[Date] ) )
-- PY
CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
-- YoY %
VAR Cur = SELECTEDMEASURE ()
VAR Prev = CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
RETURN DIVIDE ( Cur - Prev, Prev )
- Set Ordinal on items (0, 1, 2, 3) so they appear in that order.
- For YoY %, set a dynamic format string expression:
"0.0%". Other items keep the measure's own format (they returnSELECTEDMEASUREFORMATSTRING()by default).
Worked example¶
Matrix: Date[Year Month] (2025 months) on rows, Time Calc[Period] on columns, values
[Total Amount]:
| Year Month | Current | YTD | PY | YoY % |
|---|---|---|---|---|
| 2025-01 | 420 | 420 | 380 | 10.5% |
| 2025-02 | 460 | 880 | 400 | 15.0% |
| 2025-03 | 335 | 1,215 | 290 | 15.5% |
Swap the value to [Total Units] and the same four columns now compute units — no new
measures. With the Level 2 units (2025: Jan 7, Feb 9, Mar 4; 2024 rows added in lesson 05:
3, 5, 12), expected:
| Year Month | Current | YTD | PY | YoY % |
|---|---|---|---|---|
| 2025-01 | 7 | 7 | 3 | 133.3% |
| 2025-02 | 9 | 16 | 5 | 80.0% |
| 2025-03 | 4 | 20 | 12 | −66.7% |
(Jan 2025 units: rows 1–3 → 2 + 4 + 1 = 7; Feb: 1 + 2 + 6 = 9; Mar: 3 + 1 = 4.)
Using a calculation item inside a measure¶
You can apply an item from DAX by filtering the calculation group column:
This lets you keep a few named measures for convenience while the logic lives in one place.
Precedence¶
If a model has two calculation groups — say Time Calc and Currency Conversion — and both
are applied, which wraps which? Each group has a Precedence number; the higher
precedence group is applied first, i.e. it's the outer transformation, wrapping the lower one.
For "YoY % of converted amounts" you want conversion applied to the base measure inside the
YoY logic. Test with a two-row hand example before trusting any combination.
How It Actually Works¶
A calculation group is stored as a table whose column is a filter target like any other. When
a filter context contains exactly one calculation item, the engine performs calculation
item application: when a measure reference is evaluated in that context, it replaces
the measure with the item's expression, substituting SELECTEDMEASURE() with the original
measure. CALCULATE ( [Total Amount] ) under the "YTD" item becomes
CALCULATE ( CALCULATE ( [Total Amount], DATESYTD ( 'Date'[Date] ) ) ).
Three consequences of it being a replacement of measure references:
- Only explicit measures are affected. Implicit "Sum of Amount" fields have no measure reference to replace — hence "Discourage implicit measures."
- The item wraps the measure the visual asked for.
SELECTEDMEASURE()is that measure, and it's evaluated inside the item's modified filter context — so the measures it calls ([Margin]and[Sales]insideMargin %) naturally see the YTD or PY dates, just as they would inside anyCALCULATE. Where a model gets into trouble is when a measure itself references a calculation item (for example, aSales PYmeasure built with the calculation group, then used under another item of the same group) — the rules for this "sideways" interaction are subtle. Keep calculation-group logic in the group, useISSELECTEDMEASUREorSELECTEDMEASURENAMEto special-case measures that need different treatment, and verify combinations against hand-computed numbers, as in the tables above. - Multiple items selected (no single item in the filter) means no application; the measure is evaluated as is. Totals across the Period column therefore show the raw measure, which is rarely meaningful — turn off column totals.
Dynamic format strings are evaluated in the same context and returned alongside the value, so the visual can show "15.0%" for one column and "1,215" for another from the same measure.
Common mistakes¶
- Expecting calculation groups to work on implicit measures.
- Forgetting ordinals, so items appear alphabetically (Current, PY, YoY %, YTD).
- Using the Period column in a slicer with multi-select allowed — multiple items means no transformation. Force single select.
- Ignoring precedence when adding a second calculation group.
Exercise¶
- Build
Time Calcand reproduce both tables. - Add a
Rolling 3Mitem usingDATESINPERIODand verify Mar 2025 = 1,215 for amount and 20 for units. - Add a measure
Avg Price = DIVIDE ( [Total Amount], [Total Units] )and show it withYTD. Compute by hand for Feb 2025: 880 ÷ 16 = 55.00. Then explain why the YTD item gives a ratio of year-to-date totals (880 ÷ 16) rather than an average of monthly ratios (420 ÷ 7 = 60.00 and 460 ÷ 9 = 51.11): the item changes the filter context once, and both inner measures are then evaluated over January–February together.