Create Title with Period Selections using DAX in Power BI

Rick de Groot

1

Displaying selected time periods in Power BI reports is important for creating user-friendly and easy-to-understand reports. Users typically interact with reports by selecting specific time frames, such as years, months, or custom periods. Clearly showing these selected periods helps to reduce the risk of misinterpretation. This article guides you how to dynamically display these selected periods in Power BI using DAX.

1. Typical Period Selections

Power BI dashboards typically provide the flexibility for users to select various periods for their analysis. Depending on the granularity of your report you will often see:

  • Years: Users might select a single year, multiple consecutive years (e.g., 2020-2022), or non-consecutive years (e.g., 2018, 2020, 2022) to analyze their data.
  • Months: Selections can often also be made for individual months within a year, a range of consecutive months (e.g., January to June 2023), or even specific months across different years (e.g., March 2022, July 2023).
  • Days: Sometimes it can even be useful for users to select single days (e.g., 29 Jun 2024) or ranges of consecutive days (e.g., 1 – 15 Jan 2024).
  • Consecutive Periods: Users may select a sequence of periods that could span multiple months or years, such as from the middle of December 2022 to January 15 2023.

Knowing the options your users need help when building the necessary DAX. Let’s see how we can implement this.

2. Fundamental Variables

The logic for computing different period selections is elaborate. To make your life easier, it is useful to store logic into variables available for the entire measure. To start out, we need the following variables:

// Adjust this variable to change the prefix used for the period title
VAR __PeriodPrefix =                "Selected Period: "
VAR __DiverseDates =             "a range of dates in "

// Define main start and end dates
VAR __StartDate =                   MIN( 'Calendar'[Date] )
VAR __EndDate =                     MAX( 'Calendar'[Date] )

// Provide information about the current context
VAR __DaysInCurrentContext =        COUNTROWS( VALUES( 'Calendar'[Date] ) )
VAR __DaysBetween_StartAndEnd =     DATEDIFF( __StartDate, __EndDate, DAY )  + 1
VAR __IsConsecutivePeriod =         __DaysBetween_StartAndEnd = __DaysInCurrentContext

Here, we define a period prefix used to display our selections. Next, we retrieve the start and end date in our current context. And lastly, it’s useful to have some context about the current period selections. Knowing whether we have selected a consecutive period is helpful for shortening the selection string. Let’s now see how we can use these in our logic.

3. Year Selections

When analyzing your data on a relatively high level, you commonly find filters on the year level.

  • Single Year Selection
  • Multiple Years (Consecutive)
  • Multiple Years (Non Consecutive)

In such a scenario, seeing which years have been selected can be useful. You can identify three common scenarios:

3.1 Single Year Selection

The first scenario is the Single Year Selection. You could for example select only 2021. It would be easy if the user can see their selection in the subtitle of a visual. That could look like below:

Displaying Single Year Selection using DAX in Power BI

The code checks whether the current context starts and ends at the year ranges and are a single year. In that case it returns the selection string for a single year.

3.2. Consecutive Years Selection

The second scenario is where you make multiple year selections. Instead of displaying each of them individually, you can indicate separate the starting and the ending of the year range with a dash. For instance, selecting 2021, 2022, 2023 and 2024 then looks as follows:

Displaying Consecutive Years using DAX in Power BI

3.3. Non-Consecutive Years Selection

Lastly, the selecting multiple years, where not all of them are consecutive requires some more precision. We can use a pipe symbol (|) to separate regular year different year selections. That even allows you to combine regular year notations and ranges. Here’s an example:

Displaying consecutive and non-consecutive years using DAX in Power BI

So how can we create the logic for these year selections?

3.4. Year Selection DAX Code

To create a string containing the selected periods, we must think out of the box for a bit. Some considerations we will consider are:

  • Year Ranges: ideally, we show a year range with a dash between the first and the last year within the range.
  • Separate Periods: Separating year selections that are not consecutive.

Here’s how: the first step is creating a table containing your report’s unique year selections.

// Retrieve period combinations of the current filter context
VAR SelectedYears = VALUES( 'Calendar'[Year] )

With these in place, the next step is to create additional columns for this table. We want to identify the previous and next years in the current context.

You can do that by using the ADDCOLUMNS function. For the definition of the new columns, you can add:

  • @PreviousPeriod: looks at the selected years and finds the maximum period smaller than the current year.
  • @NextPeriod: looks at the selected years, and finds the minimum value that is bigger than the current year.
/* Find what the previous and next period is in the selection 
    and add the values as a column */
VAR AddPrev_Next_Year = 
    ADDCOLUMNS(
        SelectedYears,
        "@PreviousPeriod", 
        MAXX(
            FILTER( SelectedYears,
                'Calendar'[Year] < EARLIER( 'Calendar'[Year] ) ),
            'Calendar'[Year] ),
        "@NextPeriod", 
        MINX(
            FILTER( SelectedYears,
                'Calendar'[Year] > EARLIER( 'Calendar'[Year] ) ),
            'Calendar'[Year] )
    )

This is a good starting place for adding logic to our table. We can now define our logic based on whether the previous or next period combination in the context is consecutive. It should cover that:

  • The start of a range should start with the year and end with a dash “-” to indicate the beginning of a range:
    • All values within a range that return blank don’t need to be included.
    • A single month (outside of a range) or the last month in a range, which are not the last selected year, should both show a year and a pipe symbol (|) as a separator.
  • The very last combination should end with a regular year value.
VAR __ConcatenationLogic = 
  ADDCOLUMNS( 
    AddPrev_Next_Year,
    "@PartOfRange",
    VAR __NextPeriod =  'Calendar'[Year] + 1
    VAR __PrevPeriod =  'Calendar'[Year] - 1
    VAR __StringElements = 
      SWITCH( 
        TRUE(),
        ISBLANK( [@NextPeriod] ), 											 
          'Calendar'[Year], 	     // The last period in the context
        __NextPeriod <> [@NextPeriod] && __PrevPeriod <> [@PreviousPeriod],  
          'Calendar'[Year] & " | " , // A period outside of a range 
        __NextPeriod <> [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],  
          'Calendar'[Year] & " | " , // The last period value in a range
        __NextPeriod =  [@NextPeriod] && __PrevPeriod <> [@PreviousPeriod],  
          'Calendar'[Year] & " - " , // The first period in a range
        __NextPeriod =  [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],  
          BLANK(), 					// A period in the middle of a range
        __NextPeriod <> [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],  
          'Calendar'[Year] & " | "   // The last period in a range
      )
RETURN __StringElements

Now, when you see this the first time, it may look abstract. What does the result of this virtual table actually look like? Using Tabular Editor’s DAX Query feature, we evaluate the table. Here’s what it looks like when selecting the years 2021 up to 2025:

Concatenation Logic for Consecutive Full Years in Power BI

In this image, we summarize all of the unique Calendar[Year] values. Then, the [@PreviousPeriod] and [@NextPeriod] columns show the previous and next values. The part that is particularly interesting is the [@PartOfRange] column. This column contains the string elements to create a selected period string.

Here’s another example that filters on a different selection.

Concatenation Logic for Full Years with a separator in Power BI

Instead of showing all consecutive years separately, it only 2021, 2023 and 2024. Again, we can find the relevant components of the selected period string in the [@PartOfRange] column for our string.

With this in place, you can concatenate each of these ‘elements’ using the CONCATENATEX function:

"Selected Period: " & 
CONCATENATEX(
  __ConcatenationLogic, 
  [@PartOfRange], 
  "", 
  'Calendar'[Year], 
  ASC 
)

The beauty of this is that it creates a string of both individual year selections and year ranges. Any year that is within the middle of a range has an empty value ("") in the [@PartOfRange] column. That means that concatenating them will not modify the output string in any way.

Just so you know – the above logic is only relevant in case our selections refer to full years only. We must identify whether the user has selected only full years to know this. Only then should we generate the string displaying the selected months.

One way to do that is to define a variable that tests whether your current filter context contains all the necessary days. Here’s how:

VAR __FiltersFullYearsOnly =
  VAR YearsSummary = 
    VALUES( 'Calendar'[Year] )
  VAR IncompleteYearFlag = 
    ADDCOLUMNS(
      YearsSummary,
      "@IsIncompleteYear",
        VAR DatesInContext = CALCULATE( COUNTROWS( 'Calendar' ) )
        VAR DatesInYear = CALCULATE( MAX( 'Calendar'[Days in Year] ) )
        VAR IncompleteYearInContext = DatesInContext <> DatesInYear
      RETURN
        IncompleteYearInContext
      )
  VAR IncompleteYears = 
    FILTER(
      IncompleteYearFlag,
      [@IsIncompleteYear] = TRUE()
    )
  VAR Result = ISEMPTY( IncompleteYears )
RETURN
    Result

The logic here is:

  • YearsSummary: generates the unique years in the current context.
  • IncompleteYearFlag: tests for each year whether the number of days in the current context equals the total number of days in the year. For incomplete years it returns a true value.
  • IncompleteYears: Keeps only incomplete years in the selection.
  • Result: Tests if there are incomplete years and returns true if there are none.

Now that now we have the __FilterFullYearsOnly variable, we can test whether we selected full years only. If you don’t fully grasp the solution yet, hold on a while longer. At the end of this article, you will find the full code for this solution.

4. Month Selections

Month selections are slightly more granular than year selections and these have more variations. Different output strings we will look at are:

  • Single year
    • Single month selection
    • Consecutive Month selection
    • Consecutive and non-consecutive months
  • Multiple years
    • Non-consecutive month selections
    • Consecutive month selection
    • Consecutive and non-consecutive selections

4.1. Single Year

Let’s first have a look at what the formatting strings look like for months within a single year.

4.1.1. Single Month Selection

When a user selects a single month, it’s useful to see both the month and the year value. That can look as follows:

Displaying Single Month Selection using DAX in Power BI

It’s the easiest form of the month selection string.

4.1.2. Consecutive Months Selection

In another scenario, your user may select multiple consecutive months in the same year. Instead of showing all the months individually, why not hide all the months between the start and the end date? The user knows what they’re looking at by reading the range start and end values separated by a dash. For instance:

Displaying consecutive full months within a year in DAX in Power BI

This selection string is convenient, short and precise.

4.1.3. Consecutive and Non-Consecutive Months Selection

You may also select consecutive and non-consecutive periods within the same year. In that case, it suffices to separate the ranges from the other periods using a pipe symbol and mention the year only once at the end of the string.

Here’s what that could look like:

Displaying consecutive and non consecutive months in a single year using DAX in Power BI

The description is still clear, shows a single year, and makes the string as short as possible.

4.2. Multiple Years

When a user selects multiple years, we must be more specific about the years they’re looking at. For those scenarios, we need different logic.

4.2.1. Non-Consecutive Months Selection

Handling multiple-month ranges across different years that are not consecutive requires a string where ranges within the same year display only the year number at the end of the range. Here’s what that looks like:

Displaying Month selections in multiple years (non-consecutive) using DAX in Power BI

4.2.2. Consecutive Months Selection

When you select a consecutive range of months over multiple years, both the range start and range end need to mention the month and the year. The image below shows what that looks like:

Displaying consecutive full months across multiple years in DAX in Power BI

4.2.3. Consecutive and Non-Consecutive Months Selection

The last scenario involves selections that mix consecutive and non-consecutive selections over multiple years. Here’s what that can look like:

Displaying consecutive and non consecutive full months across multiple years in DAX in Power BI

4.3. Month Selection DAX Logic

So, how can we create the logic for these selections? It’s very similar to what we did for year selections, just slightly more complex. For our logic, it’s useful to define some variables.

The first two variables format the start and end date in the selection in the format of “Mar 2023”, “Apr 2023”, etc.

VAR __FormattedStartMonth =     FORMAT( __StartDate, "mmm yyyy" )
VAR __FormattedEndMonth =       FORMAT( __EndDate,   "mmm yyyy" )

Next, knowing whether our filters select full months is important. That would indicate that we can refer to full months in our selection string. Here’s how we can test that:

VAR __FiltersFullMonthsOnly =
  VAR YearsSummary = SUMMARIZECOLUMNS( 'Calendar'[Year Month Number], 'Calendar'[Days In Month] )
  VAR DaysInYears = ADDCOLUMNS( YearsSummary, "@DaysInContext", CALCULATE( COUNTROWS( 'Calendar' ) ) )
  VAR IncompleteMonths = FILTER( DaysInYears, [@DaysInContext] <> 'Calendar'[Days In Month] )
  VAR Result = ISEMPTY( IncompleteMonths )
RETURN
  Result

This code first finds all unique Year-Month combinations. Then, it adds the number of days in the current filter context to each period and compares it with the days that each month has. Only if all days of the selected months are in the context will the variable return true, indicating that full months are selected.

With these in place, let’s see how we can compute the different month selections. The first step is to generate a list of Start of Month dates and to check whether we’re dealing with multiple years in the context.

    VAR SelectedDates = VALUES( 'Calendar'[Start Of Month] )
    VAR __MultipleYearsInContext = YEAR( __StartDate ) <> YEAR( __EndDate )

Since we may deal with consecutive periodsor nonconsecutive periods, it’s helpful for us to know for each Start of Month value in the selection whether the next or previous period is consecutive.

To find out what the previous and next month-year combination is, we can add a column to our virtual table with selected dates. Here’s how:

VAR AddPreviousDates = 
  ADDCOLUMNS(
      SelectedDates,
      "@PreviousMonthYear", 
      MAXX(
        FILTER(	
          SelectedDates,
          'Calendar'[Start Of Month] < EARLIER( 'Calendar'[Start Of Month] ) ),
        'Calendar'[Start Of Month] 
      ),
      "@NextMonthYear", 
      MINX(
        FILTER(	
          SelectedDates,
          'Calendar'[Start Of Month] > EARLIER( 'Calendar'[Start Of Month] ) ),
        'Calendar'[Start Of Month] 
      )
  )

If our report makes the selections from November 2022 up to March 2023, here’s what the virtual table would look like:

Previous and next period comparisons for months in DAX

In this image, you can see that 01/11/2022 does not have a previous period in the current context, while 01-03-2023 does not have a next period in its context.

Next, we need to define the logic for our selection string. Here’s what we’ll do:

  • Create new columns that show the previous and next month. Note that this is different from the earlier calculations. Those showed the previous and next month in the current filter context. These new columns show the actual previous and next month.
  • Our logic shows different strings based on the different strings we can have. The start of a range may start with May - , the end could show May 2023, while a separate month could show as May 2023 |
VAR __ConcatenationLogic = 
  ADDCOLUMNS(
    AddPreviousDates,
    "@PartOfRange",
    VAR  __NextMonth = EDATE( 'Calendar'[Start Of Month], 1 )
    VAR  __PrevMonth = EDATE( 'Calendar'[Start Of Month], - 1 )
    VAR  __StringElements = 
      SWITCH(
        TRUE(),
        /* __________________ Multiple year logic ____________________________ */
        
        ISBLANK( [@NextMonthYear] ), 
          FORMAT( 'Calendar'[Start Of Month], "mmm yyyy " ),   // The last period of all periods
        YEAR( [@NextMonthYear] ) <> YEAR( 'Calendar'[Start Of Month] ) && NOT ( __IsConsecutivePeriod ),
          FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ), // The last period in current year (in non-consecutive selection)
        
        __MultipleYearsInContext && __NextMonth <> [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear], 
          FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ), // A period outside of a range
        __MultipleYearsInContext && __NextMonth <> [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear],  
          FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ),  // Last period value in a range
        
        /* __________________ Single Year Logic   ____________________________ */
        
        __NextMonth <> [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear], 
          FORMAT( 'Calendar'[Start Of Month], "mmm | " ),      // A month outside of a range
        __NextMonth <> [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear], 
          FORMAT( 'Calendar'[Start Of Month], "mmm | " ),       // The last date/month value in a range
        
        __NextMonth = [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear] && __MultipleYearsInContext, 
          FORMAT( 'Calendar'[Start Of Month], "mmm yyyy - " ), // The first period in a range, when multiple years are selected
        __NextMonth = [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear], 
          FORMAT( 'Calendar'[Start Of Month], "mmm - " ),       // The first period in a range, with a single-year selection
        __NextMonth = [@NextMonthYear] && YEAR( [@PreviousMonthYear] ) <> YEAR( 'Calendar'[Start Of Month] ) && NOT ( __IsConsecutivePeriod ), 
          FORMAT( 'Calendar'[Start Of Month], "mmm - " ),   // A period in a range, which is the first of the year, and non-consecutive
        __NextMonth = [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear],  
          BLANK()             // A month in the middle of a range
      )
    RETURN
    __StringElements
  )

What this code does is, add particular elements that are needed for creating a string. For our example of November 2022 up to March 2023, the above code generates the last column in the below image:

Add Range Elements for Concatenating Months in DAX for Power BI

Focus on the values in the [@PartOfRange] column. When you concatenate this column, you get the string Nov 2022 – Mar 2023.

While the logic so far is great, it misses an important detail. If you have a range within the same year, you might run into a case where it shows:

  • Apr 2021 | Sep 2022 - Nov 2022.

In that scenario, it would be shorter (and still clear) to show:

  • Apr 2021 | Sep - Nov 2022.

To achieve that we will need a couple more steps. First, we need to compare if the start of a range is in the same year as the end of a range. Only in that scenario can we remove the year from the string that represents the start of the range.

To compare the current row (start of a range) with the end of a range, it’s important to first remove all the empty [@PartOfRange] strings in between. We can do that using:

VAR __RemoveMiddleOfRange = 
  FILTER( __ConcatenationLogic, [@PartOfRange] <> "" )

The following code takes the previous table as its base. Only for rows that are the start of a range, indicated by - , the code checks if the next value is in the same year.

If that is the case, it will extract the first three characters (the short month) and attach - to it. In all other scenarios, it will return the original [@PartOfRange] value we computed earlier.

VAR __YearRangeStartCorrection =
  ADDCOLUMNS(
    __RemoveMiddleOfRange,
    "@RangeElements",
    VAR __NextPartOfRange =
      MINX(
        FILTER(	
          __RemoveMiddleOfRange, 
          'Calendar'[Start Of Month] > EARLIER( 'Calendar'[Start Of Month] ) 
        ),
        'Calendar'[Start Of Month] 
      )
    VAR Result =
      IF( 
          RIGHT( [@PartOfRange], 2 ) = "- " 
              && YEAR( __NextPartOfRange ) = YEAR( 'Calendar'[Start Of Month] ), 
          LEFT( [@PartOfRange], 3 ) & RIGHT( [@PartOfRange], 3 ),
          [@PartOfRange]
      )
    RETURN Result
  )

With this in place, we can then concatenate our final string using:

__PeriodPrefix & 
CONCATENATEX( 
  __YearRangeStartCorrection, 
  [@RangeElements], 
  "", 
  'Calendar'[Start Of Month], 
  ASC 
)

5. Custom Periods

As the final part, we arrive at more granular selections. Sometimes, users may select custom periods that don’t align with standard months or years. These could be fiscal periods, weeks, or custom time frames on the date level. There’s a variety of ways to handle these. Since the code for these is relatively simple, we’ll leave it for you to read below.

6. Full Selection Code

For those interested in trying this out, you can use the below code. Create a new measure and paste the logic into it. Whenever you need an accurate period selection, you can now use it as a title for your visual or your page.

/* _________________________________________________________________________________________________________________________________________________________ */
// Adjust this variable to change the prefix used for the period title
VAR __PeriodPrefix =                "Selected Period: "
VAR __DiverseDates =                "a range of dates in "

// Define main start and end dates
VAR __StartDate =                   MIN( 'Calendar'[Date] )
VAR __EndDate =                     MAX( 'Calendar'[Date] )

// Provide information about the current context
VAR __DaysInCurrentContext =        COUNTROWS( 'Calendar' ) 
VAR __DaysBetween_StartAndEnd =     DATEDIFF( __StartDate, __EndDate, DAY )  + 1
VAR __IsConsecutivePeriod =         __DaysBetween_StartAndEnd = __DaysInCurrentContext

// Day Related Variables
VAR __FormattedEndDate =            FORMAT( __EndDate, "d mmm yyyy" ) 

// Month related variables
VAR __FormattedStartDateMonth =     FORMAT( __StartDate, "mmm yyyy" )
VAR __FormattedEndMonth =           FORMAT( __EndDate,   "mmm yyyy" ) // "d mmm yyyy"
VAR __FiltersFullMonthsOnly =
        VAR YearsSummary = SUMMARIZECOLUMNS( 'Calendar'[Year Month Number], 'Calendar'[Days In Month] )
        VAR DaysInYears = ADDCOLUMNS( YearsSummary, "@DaysInContext", CALCULATE( COUNTROWS( 'Calendar' ) ) )
        VAR IncompleteMonths = FILTER( DaysInYears, [@DaysInContext] <> 'Calendar'[Days In Month] )
        VAR Result = ISEMPTY( IncompleteMonths )
    RETURN
        Result

// Year related variables
VAR __IsSingleYear =                HASONEVALUE( 'Calendar'[Year] )
VAR __FiltersFullYearsOnly =
    VAR YearsSummary = 
        VALUES( 'Calendar'[Year] )
    VAR IncompleteYearFlag = 
        ADDCOLUMNS(
            YearsSummary,
            "@IsIncompleteYear",
                VAR DatesInContext = CALCULATE( COUNTROWS( 'Calendar' ) )
                VAR DatesInYear = CALCULATE( MAX( 'Calendar'[Days in Year] ) )
                VAR IncompleteYearInContext = DatesInContext <> DatesInYear
            RETURN
                IncompleteYearInContext
            )
    VAR IncompleteYears = 
        FILTER(
            IncompleteYearFlag,
            [@IsIncompleteYear] = TRUE()
            )
    VAR Result = ISEMPTY( IncompleteYears )
RETURN
    Result
VAR __Result = 
    SWITCH( TRUE(),
/* _________________________________________________________________________________________________________________________________________________________  
   Logic for creating a YEAR selection string */
   
   // Perform this logic only if full years are selected
    __FiltersFullYearsOnly, 
    
    // Retrieve all Period Combinations that exist in the current filter context
    VAR SelectedYears = VALUES( 'Calendar'[Year] )
    // Find what the previous and next period is in the selection and add the values as a column
    VAR AddPrev_Next_Year = 
        ADDCOLUMNS(
            SelectedYears,
            "@PreviousPeriod", 
            MAXX(
                FILTER(	SelectedYears,
                    'Calendar'[Year] < EARLIER( 'Calendar'[Year] ) ),
                'Calendar'[Year] ),
            "@NextPeriod", 
            MINX(
                FILTER(	SelectedYears,
                    'Calendar'[Year] > EARLIER( 'Calendar'[Year] ) ),
                'Calendar'[Year] )
        )
    /* Based on whether the previous or next period combination in the context is consecutive, define logic.
        - The very last combination should end with a regular year value.
        - The start of a range should start with a period and end with a dash "-" to indicate the start of a range
        - All values within a range return blank, they don't need to be included
        - A single month (outside of a range) or the last month in a range should both show: Period and a separator ( pipe symbol ) */
        
    VAR __ConcatenationLogic = 
        ADDCOLUMNS( 
            AddPrev_Next_Year,
            "@PartOfRange",
                VAR __NextPeriod =  'Calendar'[Year] + 1
                VAR __PrevPeriod =  'Calendar'[Year] - 1
                VAR __StringElements = 
                    SWITCH( TRUE(),
                        ISBLANK( [@NextPeriod] ), 											      'Calendar'[Year], 	        // The last period in the context
                        __NextPeriod <> [@NextPeriod] && __PrevPeriod <> [@PreviousPeriod],       'Calendar'[Year] & " | " ,    // A period outside of a range 
                        __NextPeriod <> [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],       'Calendar'[Year] & " | " ,    // The last period value in a range
                        __NextPeriod =  [@NextPeriod] && __PrevPeriod <> [@PreviousPeriod],       'Calendar'[Year] & " - " , 	// The first [Period] in a range
                        __NextPeriod =  [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],       BLANK(), 					    // A period in the middle of a range
                        __NextPeriod <> [@NextPeriod] && __PrevPeriod =  [@PreviousPeriod],       'Calendar'[Year] & " | "      // The last period in a range
                    )
                RETURN __StringElements
        )
    RETURN
       __PeriodPrefix & CONCATENATEX( __ConcatenationLogic, [@PartOfRange], "", 'Calendar'[Year], ASC ),
        
/* _________________________________________________________________________________________________________________________________________________________
    For month selections below calculation returns the right values: */
        
   // Check if users filter full months only
    __FiltersFullMonthsOnly,
    
   // Retrieve all Start of Month Combinations that exist in the current filter context
    VAR SelectedDates = VALUES( 'Calendar'[Start Of Month] )
    VAR __MultipleYearsInContext = YEAR( __StartDate ) <> YEAR( __EndDate )
    
    // Find what the previous and next month-year combination is and add the values as a column
    VAR AddPreviousDates = 
        ADDCOLUMNS(
            SelectedDates,
            "@PreviousMonthYear", 
            MAXX(
                FILTER(	SelectedDates,
                    'Calendar'[Start Of Month] < EARLIER( 'Calendar'[Start Of Month] ) ),
                'Calendar'[Start Of Month] ),
            "@NextMonthYear", 
            MINX(
                FILTER(	SelectedDates,
                    'Calendar'[Start Of Month] > EARLIER( 'Calendar'[Start Of Month] ) ),
                'Calendar'[Start Of Month] )
        )
    /* Based on whether the previous or next month-year combination in the context is consecutive, define logic.
        - The very last combination should end with both the month (short) and the year value.
        - The start of a range should start with month (short) and end with a dash "-" to indicate the start of a range
        - All values within a range return blank, they don't need to be included
        - A single month (outside of a range) or the last month in a range should both show: Month (short) and a separator (pipe symbol ) */

    VAR __ConcatenationLogic = 
        ADDCOLUMNS(
            AddPreviousDates,
            "@PartOfRange",
            VAR  __NextMonth = EDATE( 'Calendar'[Start Of Month], 1 )
            VAR  __PrevMonth = EDATE( 'Calendar'[Start Of Month], - 1 )
            VAR  __StringElements = 
            SWITCH(
                TRUE(),
                /* __________________ Multiple year logic ____________________________ */
                
                ISBLANK( [@NextMonthYear] ), FORMAT( 'Calendar'[Start Of Month], "mmm yyyy " ),   // The last period of all periods
                
                __NextMonth = [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear] && __MultipleYearsInContext, FORMAT( 'Calendar'[Start Of Month], "mmm yyyy - " ),     // The first period in a range, when multiple years are selected


                YEAR( [@NextMonthYear] ) <> YEAR( 'Calendar'[Start Of Month] ) && NOT ( __IsConsecutivePeriod ), FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ),       // The last period in current year (in non consecutive selection)
                
                __MultipleYearsInContext && __NextMonth <> [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear], FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ),    // A period outside of a range
                __MultipleYearsInContext && __NextMonth <> [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear], FORMAT( 'Calendar'[Start Of Month], "mmm yyyy | " ),     // Last period value in a range
                
                /* __________________ Single Year Logic   ____________________________ */
                
                __NextMonth <> [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear], FORMAT( 'Calendar'[Start Of Month], "mmm | " ),     // A month outside of a range
                __NextMonth <> [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear],  FORMAT( 'Calendar'[Start Of Month], "mmm | " ),     // The last date/month value in a range
                __NextMonth = [@NextMonthYear] && __PrevMonth <> [@PreviousMonthYear],  FORMAT( 'Calendar'[Start Of Month], "mmm - " ),     // The first period in a range, with a single year selection
                __NextMonth = [@NextMonthYear] && __PrevMonth = [@PreviousMonthYear],   BLANK()                                             // A month in the middle of a range
            )
            RETURN
            __StringElements
        )
    VAR __RemoveMiddleOfRange = FILTER( __ConcatenationLogic, [@PartOfRange] <> "" )
    VAR __YearRangeStartCorrection =
        ADDCOLUMNS(
            __RemoveMiddleOfRange,
            "@RangeElements",
            VAR __NextPartOfRange =
                MINX(
                    FILTER(	__RemoveMiddleOfRange, 'Calendar'[Start Of Month] > EARLIER( 'Calendar'[Start Of Month] ) ),
                    'Calendar'[Start Of Month] 
                )
            VAR Result =
            IF( 
                RIGHT( [@PartOfRange], 2 ) = "- " 
                    && YEAR( __NextPartOfRange ) = YEAR( 'Calendar'[Start Of Month] ), 
                LEFT( [@PartOfRange], 3 ) & RIGHT( [@PartOfRange], 3 ),
                [@PartOfRange]
            )
            RETURN Result
        )
                    
                    
    RETURN
       __PeriodPrefix & CONCATENATEX( __YearRangeStartCorrection, [@RangeElements], "", 'Calendar'[Start Of Month], ASC ),
       
/* _________________________________________________________________________________________________________________________________________________________ 
        When the selection consists of a single day, simply return the day */

        __DaysInCurrentContext = 1,
        __PeriodPrefix & FORMAT( VALUES( 'Calendar'[Date] ), "d mmm yyyy" ),
       
/* _________________________________________________________________________________________________________________________________________________________
         Define non-consecutive periods within the same month , e.g. 1 jan 2023, 5 jan 2023, 6 jan 2023 -> 
         returns: niet openeenvolgende datums in Jan 2023 */
                VAR SameYearMonth =      __FormattedStartDateMonth = __FormattedEndMonth 
                RETURN  
        __IsSingleYear &&  SameYearMonth && NOT( __IsConsecutivePeriod ),
           __PeriodPrefix & __DiverseDates & __FormattedStartDateMonth,

/* _________________________________________________________________________________________________________________________________________________________
         Define consecutive periods within the same month , e.g. 1 - 3 jan 2023
         returns: niet openeenvolgende datums in Jan 2023 */
                VAR SameYearMonth =      __FormattedStartDateMonth = __FormattedEndMonth 

                RETURN  
        __IsSingleYear &&  SameYearMonth &&  __IsConsecutivePeriod,
                VAR __FormattedStartDayShort = FORMAT( __StartDate, "d" )
                VAR __FormattedEndDayMonth = FORMAT( __EndDate, "d mmm yyyy" )
            RETURN
        __PeriodPrefix & __FormattedStartDayShort & " - " & __FormattedEndDayMonth,

/* _________________________________________________________________________________________________________________________________________________________
        Define consecutive date range within the year where end date is not end of the month, but start date is start of the month. 
        In those cases, return the short full date format of both the start and end date , e.g. 1 jan 2023 t/m 19 feb 2023-> 
        returns: 1 jan 2023 - 19 feb 2023*/
            VAR __DatesNotStartTillEndOfMonth =      __EndDate <>  EOMONTH( __EndDate, 0 ) || __StartDate =  ( EOMONTH( __StartDate, - 1 ) + 1 )
        RETURN
        __IsSingleYear && __DatesNotStartTillEndOfMonth /* && __StartDate_IsStartOfMonth */ && __IsConsecutivePeriod,
                VAR __FormattedStartDateShort = FORMAT( __StartDate, "d mmm" ) 
            RETURN 
            __PeriodPrefix & __FormattedStartDateShort & " - " & __FormattedEndDate
          
    )
RETURN     
    __Result

7. Conclusion

In this article, you learned that there are a great number of ways to show period selections. You’re dealing with days, months and years, while at the time also accounting for year selections and ranges. While this code may not be perfect for your report, we hope it inspires you to develop more.

If you need help in developing custom solutions for your own BI environment, we invite you to drop us an email. Our team is happy to assist you.

Photo of author
About the author
Rick de Groot is a Senior BI Specialist at Datahub. He is a Microsoft MVP and the blogger behind BI Gorilla and powerquery.how. He studied Accounting at Nyenrode Business University and has been writing about Power BI since 2017.

  1. Hi Rick,

    Thanks for the amazing content. I’d like to try this, what is the date table used? I’m not 100% sure of the date table columns used in the code.

    Thanks,
    Tim

    Reply

Leave a Comment

More from the
Datahub Blog

Datahub secures ISO27001:2022 certification

Ensuring the security of information is crucial for any business. At Datahub, protecting data is ...

Securing sensitive Data in Power BI

Managing data access is a critical part of using Power BI effectively, especially when dealing with sensitive information. RLS and OLS cannot be combined, but there is a way to model RLS in a way that it acts like OLS.

Data Mesh: de toekomst van schaalbare data-architectuur

Data is voor veel bedrijven de motor achter innovatie en besluitvorming. Naarmate organisaties groeien, neemt ...