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.

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 & "&#10;" &
                    "Type: " & _CurrentRowLabel & "&#10;" &
                    "Bin: " & _CurrentBin & "&#10;" &
                    "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

First, we need to acquire the HTML Content custom visual. Navigate to the Build visual pane, click the ellipsis (...) icon, and select Get more visuals. In the marketplace window, search for the visual named "HTML Content" (as shown in the image below) and add it to your report.
Next, insert the DAX measure we just created into this visual, as illustrated in the following image:
Finally, here is the result we achieved:

Github style heatmap - HTML content

Default heatmap - Matrix visual

(For comparision)

Discussion

Yes, as you can see, it is entirely possible to create a GitHub-style heatmap in Power BI using the HTML Content visual to significantly elevate the user experience. This is an incredibly powerful custom visual that empowers us to leverage raw HTML for data visualization.

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.