Level Up Your Heatmap Visuals | HTML x AI
Introduction
Heatmaps are an incredibly intuitive way to visualize data. When designing reports in Power BI, we have the option to use conditional formatting to transform a table visual into a heatmap. However, compared to other visualization platforms, the user experience (UX) here feels a bit lacking. This is especially true given how powerful programming visualization libraries have become - nowadays, you can simply chat with an AI to render charts or generate sophisticated dashboards instantly.
Looking back at the default Power BI heatmap, honestly, it feels a bit "basic." I once worked on an outsourced Power BI implementation project where the client complained quite a bit about the standard heatmap, comparing it unfavorably to other platforms. From my perspective, with careful formatting, the result is "acceptable" but hardly stands out. However, transforming a default heatmap into a GitHub-style contribution graph creates a real "wow" factor for the user experience.
In this post, I’ll show you how to leverage AI to quickly upgrade a standard Power BI heatmap into a GitHub-style contribution graph.
Summary
We will use a prompt to get Gemini to write a DAX measure specifically for the "HTML Content" visual. The prompt details and the resulting measure are provided in the section below.
Implementation in Power BI
Dataset
We will be using a synthetic but scientifically grounded log of user interactions, specifically curated with Binary Gender (Male/Female) demographics. The dataset contains 8,000 records representing a diverse range of social media users.
- Source: Social Media & Mental Health
The Logic
Our goal is to output a GitHub-style contribution heatmap where:
- Columns: Represent social media usage hours (binned into 0.5-hour intervals).
- Rows: Represent the Dominant Content Type.
- Small Multiples: The context will be split by Gender.
- Color Theme: For this example, I will use a specific blue tone: #009CDC.
Initial Experiment
To start, we can describe the general concept to the AI using the following high-level prompt:
Role: Expert Power BI Developer & DAX Specialist.
Task: Create a DAX measure to render a GitHub-style Heatmap using the "HTML
Content" custom visual.
- Columns: social_media_mental_health[Daily_Screen_Time_Hours (bins)]
- Rows: social_media_mental_health[Dominant_Content_Type]
- Cell Value: Measure [Count User]
- Small Multiple Context: social_media_mental_health[Gender]
Try a specific prompt
The initial result will likely require a few rounds of tuning to achieve the desired output. Since there are several specific design parameters involved (CSS grid, padding, colors), the AI might try to guess these for you - sometimes successfully, sometimes not. The prompt above serves as a solid starting point, but for a production-ready result, you can refer to the detailed prompt in the section below:
Role: Expert Power BI Developer & DAX Specialist.
Task: Create a DAX measure to render a GitHub-style Heatmap using the "HTML Content" custom visual.
1. Data Model Context
Table Name: social_media_mental_health
Primary Value: [Count User]
Dimensions:
Small Multiple Context: social_media_mental_health.`Gender`
Y-Axis (Rows): social_media_mental_health.`Dominant_Content_Type`
X-Axis (Columns): social_media_mental_health.`Daily_Screen_Time_Hours (bins)`
2. Technical DAX Requirements (Crucial)
- Context Retrieval: Capture the current Gender using SELECTEDVALUE (Default: "Total") for the title.
- Global Max Logic: Calculate the maximum value across the entire dataset (respecting external slicers via ALLSELECTED) to ensure consistent color scaling across all small multiples.
- Method: Use CALCULATETABLE, ADDCOLUMNS, SUMMARIZE grouping by Gender, Content Type, and Bins, then find the MAXX.
- Fixed X-Axis Logic: Create a variable _FixedBinsTable using CALCULATETABLE(VALUES(...), ALLSELECTED(...)). You must use this variable to iterate through columns in both the Header and the Body loops. This ensures all charts have identical grid columns even if data is missing.
- Constraint: You must iterate over this specific variable for both the Table Header and Body Cells. This ensures the grid structure remains rigid even if specific bins have no data.
3. Color Interpolation Logic
- Base Color: White (RGB: 255, 255, 255) for value 0/Blank.
- Target Color: Blue (RGB: 0, 156, 220) for the Global Max value.
- Algorithm: Calculate the ratio (CurrentValue / GlobalMax). Apply this ratio to the Delta of R, G, and B values to generate the dynamic hex/rgb string.
4. HTML & CSS Styling Specification
Construct the HTML string using the following structure:
A. Internal CSS (_CSS variable):
- Define classes: .hm-cell, .hm-header, .hm-row-label.
- Table: border-collapse: separate, border-spacing: 4px, padding-bottom: 15px.
- Cells: 30px x 30px placeholders (regardless of the column header width), Border Radius 5px.
B. HTML Structure:
Title: use <div> with font-size 14px, bold, color #0075A5, left border 4px solid #009CDC.
Table Header:
- First cell: "Content Type" (Left aligned, min-width 120px, font-size 12px).
- Loop through _FixedBinsTable: Create <th> cells (Center aligned, color #666).
Table Body:
- Outer Loop: Iterate through VALUES('social_media_mental_health'[Dominant_Content_Type]).
- Row Label: use <td> with white-space: nowrap.
- Inner Loop: Iterate through _FixedBinsTable (Perform Context Transition here).
Data Cells:
- Dimensions: 30px x 30px.
- Style: Border 1px solid #eee, Radius 5px, Background-color = [Calculated RGB].
- Tooltip: Must show Gender, Type, Bin, and formatted User Count.
Output: Provide only the clean, well-formatted DAX code.
The result for prompt above:
HTML Heatmap =
VAR _Title = SELECTEDVALUE('social_media_mental_health'[Gender], "Total")
-- 1. GLOBAL MAX LOGIC (Crucial for consistent coloring across Small Multiples)
VAR _GlobalStats =
CALCULATETABLE(
ADDCOLUMNS(
SUMMARIZE(
'social_media_mental_health',
'social_media_mental_health'[Gender],
'social_media_mental_health'[Dominant_Content_Type],
'social_media_mental_health'[Daily_Screen_Time_Hours (bins)]
),
"@Val", [Count User]
),
ALLSELECTED('social_media_mental_health')
)
VAR _MaxVal = MAXX(_GlobalStats, [@Val])
-- 2. FIXED BINS LOGIC (Ensures grid structure consistency)
VAR _FixedBinsTable =
CALCULATETABLE(
VALUES('social_media_mental_health'[Daily_Screen_Time_Hours (bins)]),
ALLSELECTED('social_media_mental_health'[Daily_Screen_Time_Hours (bins)])
)
-- 3. COLOR LOGIC (Target: 0, 156, 220 | Base: 255, 255, 255)
VAR _R_Delta = 0 - 255
VAR _G_Delta = 156 - 255
VAR _B_Delta = 220 - 255
-- 4. CSS STYLING
VAR _CSS = "
<style>
.hm-container { font-family: 'Segoe UI', sans-serif; padding: 5px; }
.hm-title {
font-size: 14px;
font-weight: bold;
color: #0075A5;
border-left: 4px solid #009CDC;
padding-left: 8px;
margin-bottom: 10px;
}
.hm-table {
border-collapse: separate;
border-spacing: 4px;
padding-bottom: 15px;
}
.hm-header-cell {
text-align: center;
font-size: 11px;
color: #666;
padding-bottom: 4px;
}
.hm-row-label {
font-size: 12px;
text-align: left;
white-space: nowrap;
padding-right: 10px;
color: #333;
font-weight: 500;
}
.hm-data-cell {
width: 20px;
height: 20px;
border-radius: 3.5px;
border: 1px solid #eee;
}
</style>
"
-- 5. BUILD HEADER ROW
VAR _HeaderRow =
"<tr>
<th style='text-align:left; min-width:120px; font-size:12px;'>Content Type</th>" &
CONCATENATEX(
_FixedBinsTable,
"<th class='hm-header-cell'>" & 'social_media_mental_health'[Daily_Screen_Time_Hours (bins)] & "</th>",
"",
'social_media_mental_health'[Daily_Screen_Time_Hours (bins)], ASC
) &
"</tr>"
-- 6. BUILD BODY ROWS
VAR _BodyRows =
CONCATENATEX(
VALUES('social_media_mental_health'[Dominant_Content_Type]),
VAR _CurrentRowLabel = 'social_media_mental_health'[Dominant_Content_Type]
RETURN
"<tr>
<td class='hm-row-label'>" & _CurrentRowLabel & "</td>" &
-- Inner Loop: Iterate through Fixed Bins for Cells
CONCATENATEX(
_FixedBinsTable,
VAR _CurrentBin = 'social_media_mental_health'[Daily_Screen_Time_Hours (bins)]
VAR _Val = [Count User]
-- Color Calculation
VAR _Ratio = DIVIDE(_Val, _MaxVal, 0)
VAR _R = INT(255 + (_Ratio * _R_Delta))
VAR _G = INT(255 + (_Ratio * _G_Delta))
VAR _B = INT(255 + (_Ratio * _B_Delta))
VAR _ColorString = "rgb(" & _R & "," & _G & "," & _B & ")"
VAR _FinalColor = IF(ISBLANK(_Val) || _Val = 0, "rgb(255,255,255)", _ColorString)
-- Tooltip Content
VAR _Tooltip =
"Gender: " & _Title & " " &
"Type: " & _CurrentRowLabel & " " &
"Bin: " & _CurrentBin & " " &
"Users: " & FORMAT(_Val, "#,0")
RETURN
"<td class='hm-data-cell' " &
"style='background-color:" & _FinalColor & ";' " &
"title='" & _Tooltip & "'>" &
"</td>",
"",
'social_media_mental_health'[Daily_Screen_Time_Hours (bins)], ASC
) &
"</tr>",
"",
'social_media_mental_health'[Dominant_Content_Type], ASC
)
-- 7. FINAL OUTPUT
RETURN
_CSS &
"<div class='hm-container'>" &
"<div class='hm-title'>" & _Title & "</div>" &
"<table class='hm-table'>" &
_HeaderRow &
_BodyRows &
"</table>" &
"</div>"
See the result
Furthermore, prompting AI to generate the necessary DAX measures has become easier than ever as AI models grow increasingly sophisticated. You don't necessarily need deep expertise in HTML coding; however, having a general understanding of HTML structure allows you to specify parameters more precisely, helping the AI generate accurate results faster.
