# Tableau Table Calculations to DAX: Running Totals, Ranks, and Window Functions

> Convert RUNNING_SUM, WINDOW_AVG, RANK, INDEX and LOOKUP to Power BI DAX using CALCULATE, RANKX and the OFFSET, WINDOW and ROWNUMBER window functions.

- Canonical: https://getantares.io/tableau-table-calculations-to-dax/
- Published: 2026-02-18
- Updated: 2026-07-27

---

Tableau table calculations run on the result set after aggregation, using the layout of the view to define direction and scope. DAX has no result set to walk, so every conversion rebuilds "Compute Using" as an explicit filter or window definition. The classic approach wraps `CALCULATE` around a `FILTER`. Newer DAX window functions (`OFFSET`, `WINDOW`, `ROWNUMBER`) express the same logic more directly where your Power BI version supports them.

## Table calculation to DAX conversion table

| Tableau | DAX equivalent | Approach |
| --- | --- | --- |
| `RUNNING_SUM(SUM([Sales]))` | `CALCULATE(SUM(Sales[Amount]), FILTER(ALLSELECTED('Date'[Date]), 'Date'[Date] <= MAX('Date'[Date])))` | Cumulative filter |
| `WINDOW_AVG(SUM([Sales]))` | `AVERAGEX(ALLSELECTED('Date'[Month]), CALCULATE(SUM(Sales[Amount])))` | Iterate the partition |
| `WINDOW_AVG(SUM([Sales]), -2, 0)` | `AVERAGEX(WINDOW(-2, REL, 0, REL, …), CALCULATE(SUM(Sales[Amount])))` | Window function |
| `TOTAL(SUM([Sales]))` | `CALCULATE(SUM(Sales[Amount]), ALLSELECTED())` | Whole-partition total |
| `RANK(SUM([Sales]))` | `RANKX(ALLSELECTED(Product[Product]), CALCULATE(SUM(Sales[Amount])))` | Ranking |
| `INDEX()` | `ROWNUMBER(ALLSELECTED(…), ORDERBY(…))` | Positional |
| `SIZE()` | `COUNTROWS(ALLSELECTED(Product[Product]))` | Positional |
| `LOOKUP(SUM([Sales]), -1)` | `CALCULATE(SUM(Sales[Amount]), OFFSET(-1, ALLSELECTED('Date'[Month]), ORDERBY('Date'[Month])))` | Offset |
| `PERCENT_OF_TOTAL(SUM([Sales]))` | `DIVIDE(SUM(Sales[Amount]), CALCULATE(SUM(Sales[Amount]), ALLSELECTED(Product[Product])))` | Ratio to partition |
| `PREVIOUS_VALUE()` | — | Recursive; no DAX equivalent |

## Running totals and moving averages

A running total is a cumulative filter: sum everything up to and including the current row's position on the axis. Two details decide whether it matches Tableau. Use `ALLSELECTED` rather than `ALL`, so the running total stays inside the user's slicer selection the way Tableau's default addressing does. And read the current position with `MAX('Date'[Date])` — that idiom stands in for "the current row" when there is no row context. If the Tableau calculation restarts on a dimension ("Restarting every Year"), add that column back as a filter argument so it survives `ALLSELECTED`.

Moving averages need a bounded window rather than an open-ended one. The [WINDOW](https://learn.microsoft.com/en-us/dax/window-function-dax) function takes relative or absolute boundaries plus an explicit sort, so `WINDOW(-2, REL, 0, REL, …)` is the direct equivalent of Tableau's `-2, 0` offsets. Without window functions, the equivalent is `AVERAGEX` over `DATESINPERIOD`, which requires a date table. With no offsets at all, the window is the whole partition and the conversion collapses to an iterator over `ALLSELECTED` — the same shape covers `WINDOW_SUM`, `WINDOW_MIN`, `WINDOW_MAX`, and `WINDOW_MEDIAN`.

## Ranks and positions

Tableau's five ranking functions differ mainly in how they treat ties, and [RANKX](https://learn.microsoft.com/en-us/dax/rankx-function-dax) covers most of them through its optional `ties` argument. Note that DAX has its own functions called `RANK` and `INDEX`; neither is the equivalent of the Tableau function with the same name. Both languages rank from the highest value down by default, and `Skip` ties are the RANKX default, so the plain form already matches Tableau's `RANK`. Watch the direction on `RANK_PERCENTILE`, though: Tableau puts the *lowest* value at 0 and the highest at 1, which is the reverse of a descending rank.

| Tableau | Ties produce | DAX |
| --- | --- | --- |
| `RANK` | 1, 2, 2, 4 | `RANKX(…)` — `Skip` is the default |
| `RANK_DENSE` | 1, 2, 2, 3 | `RANKX(table, expr, , , "Dense")` — `ties` is the fifth argument |
| `RANK_UNIQUE` | 1, 2, 3, 4 | `ROWNUMBER(…, ORDERBY(…))` |
| `RANK_MODIFIED` | 1, 3, 3, 4 | No direct form — derive from `RANKX` and the tie count |
| `RANK_PERCENTILE` | 0–1 scale, lowest value is 0 | `DIVIDE(COUNTROWS(ALLSELECTED(…)) - RANKX(…), COUNTROWS(ALLSELECTED(…)) - 1)` |

`INDEX()` returns a position in the addressing order rather than a rank by value, so its equivalent is `ROWNUMBER`, not `RANKX`. From there, `SIZE()` is `COUNTROWS(ALLSELECTED(…))`, `FIRST()` is `1 - ROWNUMBER(…)`, and `LAST()` is `COUNTROWS(ALLSELECTED(…)) - ROWNUMBER(…)`.

## LOOKUP and PREVIOUS_VALUE

`LOOKUP` fetches a value from another position in the partition, and [OFFSET](https://learn.microsoft.com/en-us/dax/offset-function-dax) is the direct translation. When the offset is genuinely a time shift and a proper date table exists, time intelligence is clearer and faster — `PREVIOUSMONTH` or `DATEADD`. The difference matters when periods are missing from the data: `OFFSET` steps to the previous row that exists, time intelligence steps to the previous calendar month whether or not it has data.

`PREVIOUS_VALUE()` has no DAX equivalent. It is recursive by nature: each row reads the calculation's own output from the row before it, and a DAX measure cannot reference its own prior result. Every occurrence needs restructuring, usually into a running aggregate or a Power Query step that materialises the sequence at load time. These are the most expensive items in a table-calculation inventory and should be scoped separately.

## Compute Using: addressing and partitioning in DAX

The Compute Using dialog is the whole configuration of a table calculation, and it is not stored in the formula text. Two calculations with identical text return different numbers if their addressing differs, which is why reading formulas alone is not enough to plan this work — you have to open the workbook.

| Tableau concept | What it controls | DAX equivalent |
| --- | --- | --- |
| Addressing (Compute Using) | The direction the calculation moves | [`ORDERBY()`](https://learn.microsoft.com/en-us/dax/orderby-function-dax) |
| Partitioning (Restarting every) | Where the calculation resets | [`PARTITIONBY()`](https://learn.microsoft.com/en-us/dax/partitionby-function-dax) |
| Table (across) | Walk columns, reset per row | `ORDERBY` on the column field, `PARTITIONBY` on the row field |
| Table (down) | Walk rows, reset per column | The reverse pairing |
| Specific dimensions | An explicit list, with a sort order | `ORDERBY` plus `PARTITIONBY` naming those columns |
| Pane versus Table scope | Whether an outer dimension resets it | Add the outer column to `PARTITIONBY`, or leave it out |

## What Antares does with table calculations

Table calculations are not auto-converted — they are reported as manual DAX work, alongside LOD expressions and the other structural blockers. What the Analyzer does is inventory them precisely: each one is identified by kind, so a running total is not counted the same as a recursive calculation. It also detects Quick Table Calcs, which never appear as function names in the formula text and are therefore missed by any count based on searching formulas.

That distinction is what makes an estimate defensible. A workbook with thirty running totals is a day of mechanical work; a workbook with three `PREVIOUS_VALUE` calculations is a modelling problem. Both look identical in a raw formula count. [Run the free Analyzer](https://try.getantares.io) to get the breakdown for your own estate.

Related reading: [Tableau LOD expressions to DAX](/tableau-lod-expressions-to-dax/) and [Tableau calculated fields to DAX](/tableau-calculated-fields-to-dax/). Primary sources: Tableau's [table calculation function reference](https://help.tableau.com/current/pro/desktop/en-us/functions_functions_tablecalculation.htm) and Microsoft's [DAX function reference](https://learn.microsoft.com/en-us/dax/dax-function-reference).

## Related resources

- [Tableau to Power BI Migration Guide](/tableau-to-power-bi-migration/)
- [LOD Expressions to DAX](/tableau-lod-expressions-to-dax/)
- [Calculated Fields to DAX](/tableau-calculated-fields-to-dax/)
- [Sets to DAX Measures](/tableau-sets-to-dax-measures/)

## FAQ

### What is the DAX equivalent of RUNNING_SUM?

A cumulative filter: CALCULATE(SUM(Sales[Amount]), FILTER(ALLSELECTED('Date'[Date]), 'Date'[Date] <= MAX('Date'[Date]))). ALLSELECTED keeps the running total inside the user's slicer selection, matching Tableau's default addressing, and MAX('Date'[Date]) stands in for the current row's position.

### What is the DAX equivalent of Tableau's Compute Using setting?

ORDERBY and PARTITIONBY. Addressing — the direction a calculation moves — becomes ORDERBY; partitioning, or Restarting every, becomes PARTITIONBY. Compute Using is not stored in the formula text, so two identical Tableau formulas can return different numbers and must be inspected in the workbook.

### Can PREVIOUS_VALUE be converted to DAX?

No. PREVIOUS_VALUE is recursive — each row reads the calculation's own output from the previous row — and a DAX measure cannot reference its own prior result. Every occurrence needs restructuring, usually into a running aggregate or a Power Query step that materialises the sequence at load time.

### Does Antares convert Tableau table calculations automatically?

No — they are reported as manual DAX work. What the Analyzer does is inventory them by kind, so a running total is not counted the same as a recursive calculation, and it detects Quick Table Calcs, which never appear as function names in the formula text and are missed by any count based on searching formulas.
