Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Get Fabric certified for FREE! Don't miss your chance! Learn more

mh2587

Building Dynamic Sales KPI Cards in Power BI Using SVG and DAX

In this article, I’ll walk through how I built a Sales KPI Card that displays total sales, monthly, quarterly, and yearly comparisons, along with key trend indicators — all in a single polished visual.
Why Use SVG in Power BI?
SVG allows you to:

  • Create precise, pixel-perfect visuals
  • Combine multiple KPIs in a single card
  • Add gradients, mini bars, and highlights
  • Fully control layout, spacing, and colors
  • Keep visuals dynamic and filter-aware

    The best part? You don’t need custom visuals or external tools — everything can be done using a DAX measure.

    Step 1: Core Sales Calculations
    The first step is to calculate the main sales figures:

    mh2587_0-1769418758614.png

    These values form the basis for the entire visual.

    Step 2: Calculating Trends

    Next, we calculate the month-over-month (MoM), quarter-over-quarter (QoQ), and year-over-year (YoY) growth. To avoid misleading numbers, we cap the percentages at ±100%:

    mh2587_1-1769418814182.png

    This ensures the percentages remain clear and visually accurate.

    Step 3: Visual Encoding

    We use simple visual indicators for trends:

    • Positive growth is shown in green
    • Negative growth is shown in red
    • Triangles indicate the direction of the trend

      mh2587_2-1769418874354.png

      This gives users an instant understanding of performance.

      Step 4: Highlighting the Best KPI

      To make the card even more informative, we automatically highlight the best-performing KPI:

      mh2587_3-1769418925554.png

      The card dynamically adjusts as data changes, ensuring that the most significant trend stands out.

      Step 5: Mini Bars for Context

      To provide visual scale, we include small bars for monthly, quarterly, and yearly sales. These bars are proportional to the largest value:

      mh2587_4-1769418987951.png

      Step 6: Structured Layout with SVG

      The card is divided into three logical sections:

      1. Total Sales Header — a dark blue container for immediate focus
      2. KPI Detail Section — shows last month, quarter, and year with mini bars and trend indicators; highlights the best KPI
      3. Trend Summary Section — a compact strip showing MoM, QoQ, and YoY growth percentages

        This layout makes the card modern, clean, and easy to interpret at a glance. and it will look something like the following:

        mh2587_5-1769419035183.png

        Step 7: Complete Measure Code

        Sales KPI Card Polished SVG Fixed  =
        VAR TotalSales = CALCULATE(SUM(fct_Orders[Sales]))
        VAR LastMonthSales = CALCULATE(SUM(fct_Orders[Sales]), DATEADD('dim-Date'[Date], -1, MONTH))
        VAR LastQtrSales = CALCULATE(SUM(fct_Orders[Sales]), DATEADD('dim-Date'[Date], -1, QUARTER))
        VAR LastYearSales = CALCULATE(SUM(fct_Orders[Sales]), DATEADD('dim-Date'[Date], -1, YEAR))
        - Trends
        VAR MoM = DIVIDE(TotalSales - LastMonthSales, LastMonthSales, 0)
        VAR QoQ = DIVIDE(TotalSales - LastQtrSales, LastQtrSales, 0)
        VAR YoY = DIVIDE(TotalSales - LastYearSales, LastYearSales, 0)
        - Cap percentages ±100%
        VAR MoMDisplay = MAX(-1, MIN(1, MoM))
        VAR QoQDisplay = MAX(-1, MIN(1, QoQ))
        VAR YoYDisplay = MAX(-1, MIN(1, YoY))
        - Triangles
        VAR MoMTriangle = IF(MoM >= 0,"▲","▼")
        VAR QoQTriangle = IF(QoQ >= 0,"▲","▼")
        VAR YoYTriangle = IF(YoY >= 0,"▲","▼")
        - Colors
        VAR MoMColor = IF(MoM >= 0,"#22C55E","#EF4444")
        VAR QoQColor = IF(QoQ >= 0,"#22C55E","#EF4444")
        VAR YoYColor = IF(YoY >= 0,"#22C55E","#EF4444")
        - Best KPI highlight
        VAR MaxTrend = MAXX({MoM, QoQ, YoY}, [Value])
        VAR LMHighlight = IF(MoM = MaxTrend, "#DBEAFE", "#F3F4F6")
        VAR LQHighlight = IF(QoQ = MaxTrend, "#DBEAFE", "#F3F4F6")
        VAR LYHighlight = IF(YoY = MaxTrend, "#DBEAFE", "#F3F4F6")
        - Fonts
        VAR FontSizeMain = 22
        VAR FontSizeSub = 16
        - SVG dimensions
        VAR W = 360
        VAR H = 260
        - Mini bars
        VAR MaxVal = MAXX({LastMonthSales, LastQtrSales, LastYearSales}, [Value])
        VAR LMBar = DIVIDE(LastMonthSales, MaxVal, 0) * 100
        VAR LQBar = DIVIDE(LastQtrSales, MaxVal, 0) * 100
        VAR LYBar = DIVIDE(LastYearSales, MaxVal, 0) * 100
        VAR CenterX = 180
        RETURN
        "
        <svg xmlns='http://www.w3.org/2000/svg' width='" & W & "' height='" & H & "'>
        <! - TOTAL SALES →
        <rect x='0' y='5' width='350' height='40' rx='6' fill='#1E3A8A'/>
        <text x='15' y='32' font-size='22' font-weight='bold' fill='white'>
        💰 Total Sales: " & FORMAT(TotalSales,"$#,##0") & "
        </text>
        <! - LM / LQ / LY CONTAINER →
        <rect x='0' y='50' width='350' height='145' rx='6' fill='#F3F4F6'/>
        <! - LM →
        <rect x='5' y='55' width='340' height='35' rx='4' fill='" & LMHighlight & "'/>
        <text x='15' y='75' font-size='16'>📅 LM: " & FORMAT(LastMonthSales,"$#,##0") & "</text>
        <text x='" & CenterX & "' y='75' font-size='16' fill='" & MoMColor & "'>" & MoMTriangle & " " & FORMAT(MoMDisplay,"+0.0%;-0.0%") & "</text>
        <rect x='15' y='80' width='" & LMBar & "' height='8' rx='4' fill='url(#LMGradient)'/>
        <! - LQ →
        <rect x='5' y='95' width='340' height='35' rx='4' fill='" & LQHighlight & "'/>
        <text x='15' y='115' font-size='16'>📊 LQ: " & FORMAT(LastQtrSales,"$#,##0") & "</text>
        <text x='" & CenterX & "' y='115' font-size='16' fill='" & QoQColor & "'>" & QoQTriangle & " " & FORMAT(QoQDisplay,"+0.0%;-0.0%") & "</text>
        <rect x='15' y='120' width='" & LQBar & "' height='8' rx='4' fill='url(#LQGradient)'/>
        <! - LY →
        <rect x='5' y='135' width='340' height='35' rx='4' fill='" & LYHighlight & "'/>
        <text x='15' y='155' font-size='16'>📈 LY: " & FORMAT(LastYearSales,"$#,##0") & "</text>
        <text x='" & CenterX & "' y='155' font-size='16' fill='" & YoYColor & "'>" & YoYTriangle & " " & FORMAT(YoYDisplay,"+0.0%;-0.0%") & "</text>
        <rect x='15' y='160' width='" & LYBar & "' height='8' rx='4' fill='url(#LYGradient)'/>
        <! - TREND CONTAINER →
        <rect x='0' y='200' width='350' height='55' rx='6' fill='#1F2A44'/>
        <text x='35' y='220' font-size='14' fill='#FFFFFF'>MoM</text>
        <text x='35' y='242' font-size='18' font-weight='bold' fill='" & MoMColor & "'>
        " & MoMTriangle & " " & FORMAT(MoMDisplay,"+0%;-0%") & " |
        </text>
        <text x='140' y='220' font-size='14' fill='#FFFFFF'>QoQ</text>
        <text x='140' y='242' font-size='18' font-weight='bold' fill='" & QoQColor & "'>
        " & QoQTriangle & " " & FORMAT(QoQDisplay,"+0%;-0%") & " |
        </text>
        <text x='245' y='220' font-size='14' fill='#FFFFFF'>YoY</text>
        <text x='245' y='242' font-size='18' font-weight='bold' fill='" & YoYColor & "'>
        " & YoYTriangle & " " & FORMAT(YoYDisplay,"+0%;-0%") & "
        </text>
        <! - GRADIENTS →
        <defs>
        <linearGradient id='LMGradient' x1='0' y1='0' x2='1' y2='0'>
        <stop offset='0%' stop-color='" & IF(MoM>=0,"#22C55E","#EF4444") & "'/>
        <stop offset='100%' stop-color='" & IF(MoM>=0,"#A7F3D0","#FCA5A5") & "'/>
        </linearGradient>
        <linearGradient id='LQGradient' x1='0' y1='0' x2='1' y2='0'>
        <stop offset='0%' stop-color='" & IF(QoQ>=0,"#22C55E","#EF4444") & "'/>
        <stop offset='100%' stop-color='" & IF(QoQ>=0,"#A7F3D0","#FCA5A5") & "'/>
        </linearGradient>
        <linearGradient id='LYGradient' x1='0' y1='0' x2='1' y2='0'>
        <stop offset='0%' stop-color='" & IF(YoY>=0,"#22C55E","#EF4444") & "'/>
        <stop offset='100%' stop-color='" & IF(YoY>=0,"#A7F3D0","#FCA5A5") & "'/>
        </linearGradient>
        </defs>
        </svg>
        "

         

         

         Note: Don't forget to put this measure in HTML Content import visual.

         

        Key Takeaways

        • SVG + DAX allows for fully custom, responsive visuals.
        • Automatic highlights and trend indicators improve dashboard storytelling.
        • Mini bars provide visual context for comparisons without adding clutter.
        • The card is compact, modern, and reusable across any KPI.

          With this approach, Power BI reports can go beyond standard visuals and deliver rich, executive-friendly insights in a single card.