Recently I worked on a Power BI project for a client. They had a SharePoint site where they regularly published articles and wanted to pour view data into a report to track which pieces of content were performing well. One of their requests – sorting the matrices by different values – for the Power BI report turned out to be as tricky to implement as it seemed straightforward when I first heard it. A rare occurrence! Let’s explore the solution to this conundrum.
The whole report focused on articles that were published in their SharePoint environment. One of the key metrics was the number of views these pieces of content get, with more being better. To explain the solution in this blog, I’ve created some sample tables called ‘Articles’ that have some basic fields:
To start analyzing why articles score well, the client needed to find which articles scored well first. The logical request was as follows: please give us a table or matrix that shows the top three most-viewed articles per month, per year. The months should be sorted in descending order and the articles within those months should be sorted from most number of views to least. A more visual example:
2021 Views
February
Title 1 x3
Title 2 x2
Title 3 x1
January
Title 1 y3
Title 2 y2
Title 3 y1
2020
December
Title 1 z3
Title 2 z2
Title 3 z1
“Of course!”, I thought, “No biggie!”. A textbook example of the Dunning-Kruger effect. My Power BI skills aren’t top notch. My focus the past few years has been on Azure and the report building I’ve done has been focused on Sequel Server Reporting Services (SSRS), not Power BI. However, I do know how I could build this in SSRS and how I’ve only rarely encountered visuals in SSRS, that I couldn’t quickly replicate in Power BI. Mind set to the task, I quickly got started and before I knew it ran into the first issues.
The main one I found, is that Power BI does not allow to sort by both the rows (date, month or year) as well as another value, in this case number of views. I could either sort by number of views, which meant the dates were not in the order requested (newest, i.e., February, first):
Alternatively, I could sort by date, but this made it so that the articles weren’t ordered by number of views anymore:
After scouring the internet, I found that the only thing that could maybe help was using a RANKX()-function. Simply put, you give the function a table as context and pick – in our case – a column to sort by. If you do so, you end up with a ranking number. What I couldn’t find, however, was any blog or other page that detailed how to effectively combine this function with a matrix. After a bunch of tests, I found a solution that is a bit raggedy, but I’m happy to report it works well.
For starters, I needed the ranking for the articles. This is an obvious starting point for a ranking solution and will also be important later for filtering. So, I created this calculated column in my table:
This code creates a table with the articles grouped by YearMonth and then assigns them a rank based on the number of views. The article with highest number of views in its month gets rank 1, the second highest gets rank 2, etc. This resulted in the column on the far right:
This new column was immediately useful, as it functions well as a filter in my visual:
By limiting it to ranks 1, 2 or 3, I will only see the top 3 most-viewed articles in my visual. Perfect!
However, after playing around some more with this new column in the matrix, I concluded that it didn’t sufficiently allow me to sort in the requested way. I looked further and the idea I ended up going for, was to create a support column that I would hide in plain sight. I put the entire logic for the sorting in a single field that I named – aptly, if I’m writing my own reviews – HiddenRankMonth:
At the heart of this formula, the same calculated table is created with the articles grouped by YearMonth. Again a rank is assigned to the articles with the RANKX()-function, using the Views as the basis for the ranking. An important difference though, is that the last argument for the RANKX()-function is this time specified as 1. You could also put “ASC”, for ascending. This means that if we have 4 articles in a given month, the article with the highest number of views will get rank 4, the article with the second highest gets rank 3, the lowest is rank 1.
This might seem counter-intuitive at first, but there is a method to the madness. We need to order year and month from most recent to oldest in our visual. Since I am attempting to put all the sorting logic in one field, this forces my hand to also sort the rank in descending order. And thus, I need the article with highest number of views to also have the highest rank number. It makes more sense visually, so please do look below:
By sorting on the new HiddenRankMonth field, our data is already in the order we need it. First sorted by year (all 2021, so in this example of no consequence), month (February at the top, January lower) and then views within each month (highest first). All that remains doing is implementing in our matrix:
Adding this column, we can sort on it and now our matrix is also sorted as per the client’s request. We now have an ugly column in our visual though, not ideal. To fix this, I changed some formatting settings for the matrix visual. I turned off word wrapping for both values and column headers. Then I simply made the column as slim as the visual allows, which looks like this:
There is a tiny sliver of the extra column still visible, but only to the sharpest of eyes. I would dare say this will be unobtrusive to most. With the sorting sorted, I was chuffed with myself…
…until I noticed that for some months it wasn’t displaying a top 3, but rather a top 2. Worse even, sometimes these top 2’s were seemingly unsorted by the number of views. This is more evident in the sample data, I’m sure some readers will have noticed in the above screenshots.
This puzzled me at first. Looking into it more, I found that when:
In my sample data, I created two articles with 10/01/2021 as the date and that held rank 2 and 3 respectively in that month. In the matrix it only shows the ‘Boomer’ article, since it has the higher number of views of the two (69 vs. 42). Moreover, it displays the number of views as 111, the sum of these numbers.
To fix this, I must make the row values in my matrix unique. The most visually pleasing way I found to do this, is with the following calculated column:
This formula simply concatenates the date and rank for an article and formats them a little. The resulting new column is unique (enough):
There is a fringe case where 2 or more articles in the top 3 get published on the same date and have the exact same number of views that would not be unique. This was more than improbable enough in the context of our client’s data, that it was not a concern, and I didn’t overcomplicate it needlessly. Be wary though for the context of your use case!
Anyway, I digress. Back to our solution now, the new column implemented in the matrix, instead of just the date, looks as such:
Finally, the result we were looking for. Way harder than I anticipated when I set out. I was a bit naive there. Regardless, with some internet sleuthing and some good old trial and error, we had another happy client.
Cookie | Duration | Description |
---|---|---|
ARRAffinity | session | ARRAffinity cookie is set by Azure app service, and allows the service to choose the right instance established by a user to deliver subsequent requests made by that user. |
ARRAffinitySameSite | session | This cookie is set by Windows Azure cloud, and is used for load balancing to make sure the visitor page requests are routed to the same server in any browsing session. |
cookielawinfo-checkbox-advertisement | 1 year | Set by the GDPR Cookie Consent plugin, this cookie records the user consent for the cookies in the "Advertisement" category. |
cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
CookieLawInfoConsent | 1 year | CookieYes sets this cookie to record the default button state of the corresponding category and the status of CCPA. It works only in coordination with the primary cookie. |
elementor | never | The website's WordPress theme uses this cookie. It allows the website owner to implement or change the website's content in real-time. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | Cloudflare set the cookie to support Cloudflare Bot Management. |
pll_language | 1 year | Polylang sets this cookie to remember the language the user selects when returning to the website and get the language information when unavailable in another way. |
Cookie | Duration | Description |
---|---|---|
_ga | 1 year 1 month 4 days | Google Analytics sets this cookie to calculate visitor, session and campaign data and track site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognise unique visitors. |
_ga_* | 1 year 1 month 4 days | Google Analytics sets this cookie to store and count page views. |
_gat_gtag_UA_* | 1 minute | Google Analytics sets this cookie to store a unique user ID. |
_gid | 1 day | Google Analytics sets this cookie to store information on how visitors use a website while also creating an analytics report of the website's performance. Some of the collected data includes the number of visitors, their source, and the pages they visit anonymously. |
ai_session | 30 minutes | This is a unique anonymous session identifier cookie set by Microsoft Application Insights software to gather statistical usage and telemetry data for apps built on the Azure cloud platform. |
CONSENT | 2 years | YouTube sets this cookie via embedded YouTube videos and registers anonymous statistical data. |
vuid | 1 year 1 month 4 days | Vimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website. |
Cookie | Duration | Description |
---|---|---|
ai_user | 1 year | Microsoft Azure sets this cookie as a unique user identifier cookie, enabling counting of the number of users accessing the application over time. |
VISITOR_INFO1_LIVE | 5 months 27 days | YouTube sets this cookie to measure bandwidth, determining whether the user gets the new or old player interface. |
YSC | session | Youtube sets this cookie to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt-remote-device-id | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt.innertube::nextId | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
Cookie | Duration | Description |
---|---|---|
WFESessionId | session | No description available. |