Post

Optimizing the SVG Heatmap

Optimizing the SVG Heatmap using the Sparkline measure pattern

Optimizing the SVG Heatmap

A quick post that is the cumulation of a couple of previous posts, optimizing the heatmap SVG using the Power BI Sparklines pattern.

Performance

Running on the Fabric Log Analytics for Analysis Services Engine report template hydrated with a months worth of logs. I captured the query that generated the following matrix.

Old

Running server timing on DAX Studio you can see that the old code causes the materialization of a large data cache and uses a large amount of formula engine, taking nearly 3 seconds.

Old

Adjusting the code to a similar pattern to sparklines we reduce this by 90% to 347ms, with no large materialization.

New

Optimized Code

Command CPU (s) Heatmap SVG =
VAR __svgHeight = 20
VAR __svgWidth = 150
 
VAR _numBoxes = 40
VAR _boxWidth = __svgWidth / _numBoxes
 
VAR __Categories = VALUES( ExecutionMetrics[XmlaRequestId] )
VAR __Range =
     MAXX(
        ALLSELECTED( ExecutionMetrics[XmlaRequestId] ),
        CALCULATE(
            SUM( ExecutionMetrics[totalCpuTimeMs] ) / 1000
            ,ExecutionMetrics[LogAnalyticsCategory]= "Command"
            ,REMOVEFILTERS( Artifact )
        )
    )
 
VAR __Data =
    ADDCOLUMNS(
        KEEPFILTERS(
            FILTER(
                KEEPFILTERS( __Categories ),
                not ISBLANK(
                    CALCULATE(
                        SUM( ExecutionMetrics[totalCpuTimeMs] ) / 1000
                        ,ExecutionMetrics[LogAnalyticsCategory]= "Command"
                    )
                )
            )
        ),
        "@box",
            INT(
                (
                    CALCULATE(
                        SUM( ExecutionMetrics[totalCpuTimeMs] ) / 1000
                        ,ExecutionMetrics[LogAnalyticsCategory]= "Command"
                    ) /   __Range
                ) * (_numBoxes - 1 )
            ) + 1
    )
VAR _countPerBox =
    ADDCOLUMNS(
        SUMMARIZE( __Data, [@box] )
        ,"@x", ( [@box] * _boxWidth ) - _boxWidth // could do this elsewhere
        ,"@cnt",
            VAR _box =  [@box]
            RETURN
            CALCULATE( COUNTX ( __Data, IF( [@box] = _box && _box <> 0, 1 ) ) )
    )
VAR _cntRange = MAXX( _countPerBox, [@cnt] )
VAR __Boxes =
    CONCATENATEX(
        ADDCOLUMNS(
            _countPerBox
            ,"@Boxes"
            ,// Mapping values range 0 -> 255
            VAR _inputStart =           0                   // The lowest number of the range input
            VAR _inputEnd =             LOG( _cntRange, 10 )    // The largest number of the range input
            VAR _outputStart =          255                            // The lowest number of the range output
            VAR _outputEnd =            0                     // The largest number of the range output         
            VAR _outputVal =            _outputStart + ((_outputEnd - _outputStart) / (_inputEnd - _inputStart)) * ( LOG( [@cnt], 10 ) - _inputStart)
            // https://dax.tips/2019/10/02/dax-base-conversions/
            VAR ConvertMe = IFERROR( _outputVal, 255 )
            VAR Base = 16
            VAR BitTable = GENERATESERIES ( 1, 8 )
            VAR DEC2HEX =
                CONCATENATEX(
                    BitTable,
                    VAR c = MOD( TRUNC ( ConvertMe / POWER ( base, [value] - 1 ) ), base )
                    RETURN SWITCH(c,10,"A",11,"B",12,"C",13,"D",14,"E",15,"F",c),
                    ,[Value],Desc
                )
            VAR HEX = "#" & REPT( RIGHT( DEC2HEX, 2 ), 3 ) & IF( ConvertMe = 255, "00", "" )
            RETURN
            "<rect id='box' x='" & [@x] & "' y='" & __svgHeight / 2 & "' width='" & _boxWidth & "' height='" & __svgHeight / 2 & "' fill='" & HEX & "' filter='url(#gradient)'/>"
        )
        ,[@Boxes]
        ," "
        ,[@x]
    )
 
// Trends
var dt = MAX( Dates[Date] )
var _greenHex = "#37A794"
var _redHex = "#DD6B7F"
 
VAR __MaxVal =
    MAXX(
        VALUES( ExecutionMetrics[XmlaRequestId] ),
        CALCULATE(
            SUM( ExecutionMetrics[totalCpuTimeMs] ) / 1000
            ,ExecutionMetrics[LogAnalyticsCategory]= "Command"
        )
    )
 
// Avg Trend
var _MonthAverageAvg =
    CALCULATE(
        AVERAGEX( VALUES( ExecutionMetrics[XmlaRequestId] ), CALCULATE( SUM( ExecutionMetrics[totalCpuTimeMs] ) ) / 1000 )
        ,REMOVEFILTERS( Dates )
        ,DATESINPERIOD( Dates[Date], dt, -1, MONTH )
        ,ExecutionMetrics[LogAnalyticsCategory] = "Command"
    )
var _WeekAverageAvg =
    CALCULATE(
        AVERAGEX( VALUES( ExecutionMetrics[XmlaRequestId] ), CALCULATE( SUM( ExecutionMetrics[totalCpuTimeMs] ) ) / 1000 )
        ,REMOVEFILTERS( Dates )
        ,DATESINPERIOD( Dates[Date], dt, -7, DAY )
        ,ExecutionMetrics[LogAnalyticsCategory] = "Command"
    )
Var _trendAvg = IF( not ISBLANK( _MonthAverageAvg ), (1 - ( _WeekAverageAvg / _MonthAverageAvg )) * - 1 )
Var _trendAvgHex = IF( _trendAvg > 0, _redHex, _greenHex )
 
// 90 Percentile Trend
var _MonthAverage90th =
    CALCULATE(
        PERCENTILEX.INC( VALUES( ExecutionMetrics[XmlaRequestId] ), CALCULATE( SUM( ExecutionMetrics[totalCpuTimeMs] ) ) / 1000, 0.9)
        ,REMOVEFILTERS( Dates )
        ,DATESINPERIOD( Dates[Date], dt, -1, MONTH )
        ,ExecutionMetrics[LogAnalyticsCategory] = "Command"
    )
var _WeekAverage90th =
    CALCULATE(
        PERCENTILEX.INC( VALUES( ExecutionMetrics[XmlaRequestId] ), CALCULATE( SUM( ExecutionMetrics[totalCpuTimeMs] ) ) / 1000, 0.9)
        ,REMOVEFILTERS( Dates )
        ,DATESINPERIOD( Dates[Date], dt, -7, DAY )
        ,ExecutionMetrics[LogAnalyticsCategory] = "Command"
    )
Var _trend90th = IF( not ISBLANK( _MonthAverage90th ), (1 - ( _WeekAverage90th / _MonthAverage90th )) * - 1 )
Var _trend90thHex = IF( _trend90th > 0, _redHex, _greenHex )
 
VAR __Svg =
    "data:image/svg+xml;utf8, <svg width=""" & __svgWidth & """ height=""" & __svgHeight &""" xmlns=""http://www.w3.org/2000/svg"">" &
    "<defs>
        <filter id=""gradient"" color-interpolation-filters=""sRGB"">
            <feComponentTransfer>
                <feFuncR type=""table"" tableValues=""1 0.975"" />
                <feFuncG type=""table"" tableValues="" 0.78 0.975"" />
                <feFuncB type=""table"" tableValues=""0.2 0.975"" />
            </feComponentTransfer>
        </filter>
    </defs>" &
    "<text x='0' y='7' fill='black' font-size='6' font-family='Segoe UI, sans-serif'>Max</text>" &
    "<text x='15' y='7' fill='black' font-size='7' font-family='Segoe UI, sans-serif' font-weight='bold'>" & FORMAT(__maxVal, "0.0") & "</text>" &
    "<text x='45' y='7' fill='black' font-size='6' font-family='Segoe UI, sans-serif'>Avg Trend</text>" &
    "<text x='75' y='7' fill='" & _trendAvgHex & "' font-size='7' font-family='Segoe UI, sans-serif' font-weight='bold'>" & FORMAT(_trendAvg, "0%") & "</text>" &
    "<text x='100' y='7' fill='black' font-size='6' font-family='Segoe UI, sans-serif'>90th Trend</text>" &
    "<text x='130' y='7' fill='" & _trend90thHex & "' font-size='7' font-family='Segoe UI, sans-serif' font-weight='bold'>" & FORMAT(_trend90th, "0%") & "</text>" &
    __Boxes &
    "</svg>"
   
RETURN
IF( not ISEMPTY(__Data), __Svg )

Conclusions

This is a fantastic pattern, that is reasonably easy to implement and obtain good results. Of note I test the performance of the Violin plot, and it already very good, and doesn’t require any improvement.

This post is licensed under CC BY 4.0 by the author.