Power BI SVG Measures: Build Icons, Target Bars and Sparklines

Introduction
A table can show the right numbers and still make you work too hard to spot the problem. Power BI SVG measures let you add small, data-driven graphics beside those numbers: a status symbol, a target marker, or a trend line that changes with filters. This guide builds three reusable patterns with DAX, then explains the advanced decisions that make them reliable. You’ll keep the underlying measures numeric, handle missing data deliberately, and avoid visuals that look convincing but compare the wrong things.
Why This Matters
Imagine a monthly site-performance report. Managers need to know which sites missed their revenue target, how far away they were, and whether performance is improving. Three separate report pages can answer those questions. A well-designed table can bring them together.
SVG means Scalable Vector Graphics: an image described with XML shapes and coordinates. A DAX measure can assemble that description as text. When a compatible visual interprets the measure as an Image URL, the text becomes an image. Filter context—the active combination of row, slicer and report filters—determines the values used to draw it.
Use SVG when you need a compact visual inside a table or matrix, or precise control over a small indicator. Built-in conditional-formatting icons, data bars and sparklines are simpler options when their formatting meets your needs. SVG adds maintenance work, so give every extra shape a reporting purpose.
Choose the Most Useful SVG Pattern
Pattern | Best business question | Important design decision |
Status icon | Which rows need attention? | Use shape and a text status, not color alone |
Target bar | How close are we to plan? | Explain whether lengths show attainment or absolute value |
Sparkline | Is the trend changing? | Choose the date window, gap policy and scale |
Diverging variance bar | How far above or below plan? | Keep a common zero and symmetric scale |
Dumbbell chart | How do two values compare? | Use the same axis across comparable rows |
Start with the first three. Diverging bars and dumbbells are useful extensions once your filter logic works. A radial gauge can suit a single completion indicator, but a straight bar is usually easier to compare across many rows. These are design recommendations, not a universal ranking.
Before You Start
Use a current Power BI Desktop build and an editable semantic model. This walkthrough targets the native Table or Matrix visual; it does not assume every visual can render an SVG measure. No extra AppSource visual is required for these examples. Publishing and viewing still follow your normal Power BI licensing and access requirements.
Create an illustrative table named Performance with columns Site, Date, Actual and Target. Use one row per site and month; Date should contain that month’s first day. Create DimSite with a unique Site column, and relate DimSite[Site] to Performance[Site], one-to-many. Relate a proper DimDate[Date] calendar to Performance[Date]. Use dimension columns in visuals and slicers.
The examples assume non-negative revenue and strictly positive targets. They intentionally return blank for invalid targets; signed profit or expense indicators need a different scale. Higher attainment is treated as better. Reverse that business rule for measures such as waiting time or error rate.
Step-by-Step Solution
Step 1: Keep Business Measures Separate
Create these as separate DAX measures using New measure. Don’t paste the entire block into one measure definition.
Actual = SUM ( Performance[Actual] )
Target = SUM ( Performance[Target] )
Attainment = DIVIDE ( [Actual], [Target] )
Status Label =
VAR A = [Actual]
VAR T = [Target]
RETURN
SWITCH (
TRUE (),
ISBLANK ( A ) || ISBLANK ( T ) || T <= 0 || A < 0,
"No valid target",
A >= T, "On target",
A >= T * 0.9, "Watch",
"Below target"
)Format Attainment as a percentage. The 90% watch threshold is an illustrative business rule, so agree it with your report owner. Keep Actual, Target and Attainment numeric: you’ll need them for sorting, totals and other charts. Only the SVG measures should return image text.
Step 2: Build a Status Icon with Three Distinct Shapes
Create the following DAX measure. It uses a check mark for on target, a horizontal stroke for watch, and a downward triangle for below target. Add Status Label beside it so the meaning remains readable without color.
Status SVG =
VAR A = [Actual]
VAR T = [Target]
VAR Valid =
ISINSCOPE ( DimSite[Site] )
&& NOT ISBLANK ( A )
&& NOT ISBLANK ( T )
&& T > 0 && A >= 0
VAR Colour =
SWITCH ( TRUE (),
A >= T, "#006D35",
A >= T * 0.9, "#A45A00",
"#B42318"
)
VAR Shape =
SWITCH ( TRUE (),
A >= T,
"<path d='M7 12 L10 15 L17 8' fill='none' stroke='white' stroke-width='2'/>",
A >= T * 0.9,
"<path d='M7 12 H17' stroke='white' stroke-width='2'/>",
"<path d='M7 9 H17 L12 16 Z' fill='white'/>"
)
VAR Svg =
"<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'>"
& "<circle cx='12' cy='12' r='11' fill='" & Colour & "'/>"
& Shape & "</svg>"
RETURN
IF ( Valid,
"data:image/svg+xml;utf8," & SUBSTITUTE ( Svg, "#", "%23" )
)ISINSCOPE suppresses the icon at a grand total where Site is not a grouping level. It avoids a misleading total badge even when a slicer selects just one site. If you use a site hierarchy, adapt the guard to the level where the image should appear.
Step 3: Draw an Attainment Bar with a Target Marker
This DAX measure uses a fixed 0–120% attainment scale. The target marker is always at 100%; the full track represents 120%. It compares performance against each site’s own target, not revenue amounts between sites.
Target Bar SVG =
VAR A = [Actual]
VAR T = [Target]
VAR Valid =
ISINSCOPE ( DimSite[Site] )
&& NOT ISBLANK ( A )
&& NOT ISBLANK ( T )
&& T > 0 && A >= 0
VAR Ratio = DIVIDE ( A, T )
VAR BarWidth = 180 * MIN ( 1.2, MAX ( 0, Ratio ) ) / 1.2
VAR WidthText = FORMAT ( BarWidth, "0.0", "en-US" )
VAR Label = FORMAT ( Ratio, "0%", "en-US" )
VAR Colour = IF ( Ratio >= 1, "#006D35", "#B42318" )
VAR Overflow = IF ( Ratio > 1.2,
"<path d='M185 8 L191 14 L185 20' fill='none' stroke='#111827' stroke-width='2'/>",
""
)
VAR Svg =
"<svg xmlns='http://www.w3.org/2000/svg' width='260' height='28' viewBox='0 0 260 28'>"
& "<rect x='10' y='8' width='180' height='12' rx='3' fill='#E5E7EB'/>"
& "<rect x='10' y='8' width='" & WidthText
& "' height='12' rx='3' fill='" & Colour & "'/>"
& "<line x1='160' y1='4' x2='160' y2='24' stroke='#111827' stroke-width='2'/>"
& Overflow
& "<text x='205' y='19' font-family='sans-serif' font-size='13' fill='#111827'>"
& Label & "</text></svg>"
RETURN
IF ( Valid,
"data:image/svg+xml;utf8," & SUBSTITUTE ( Svg, "#", "%23" )
)The bar stops growing at 120%, while the label keeps the actual percentage. A chevron flags values beyond the plotted range. Keep the numeric Attainment column too: a tiny image label should not be your only precise value.
For absolute revenue comparisons, replace the ratio with Actual divided by a common maximum across the relevant sites, then position each target on that same scale. Don’t independently normalize every row to its own maximum and describe the result as a revenue ranking.

Step 4: Create a Date-Aware Sparkline
This DAX measure plots the last 12 nonblank observation dates within the current date filters. With the monthly sample table, that means up to 12 reported months. It does not force a rolling 12-calendar-month window. Put Site on rows and leave dates in a slicer, rather than splitting the trend into individual month columns.
Trend SVG =
VAR Observations =
FILTER (
ADDCOLUMNS (
VALUES ( DimDate[Date] ),
"@Actual", CALCULATE ( [Actual] )
),
NOT ISBLANK ( [@Actual] )
)
VAR Points = TOPN ( 12, Observations, DimDate[Date], DESC )
VAR N = COUNTROWS ( Points )
VAR FirstDate = MINX ( Points, DimDate[Date] )
VAR LastDate = MAXX ( Points, DimDate[Date] )
VAR Low = MINX ( Points, [@Actual] )
VAR High = MAXX ( Points, [@Actual] )
VAR Coordinates =
ADDCOLUMNS (
Points,
"@X", 5 + 150 * DIVIDE (
DimDate[Date] - FirstDate, LastDate - FirstDate, 0
),
"@Y", IF ( High = Low, 20,
35 - 30 * DIVIDE ( [@Actual] - Low, High - Low )
)
)
VAR LinePoints =
CONCATENATEX (
Coordinates,
FORMAT ( [@X], "0.0", "en-US" ) & ","
& FORMAT ( [@Y], "0.0", "en-US" ),
" ", DimDate[Date], ASC
)
VAR Svg =
"<svg xmlns='http://www.w3.org/2000/svg' width='160' height='40' viewBox='0 0 160 40'>"
& "<polyline points='" & LinePoints
& "' fill='none' stroke='#006D35' stroke-width='2'/></svg>"
RETURN
IF ( ISINSCOPE ( DimSite[Site] ) && N >= 2,
"data:image/svg+xml;utf8," & SUBSTITUTE ( Svg, "#", "%23" )
)Sorting inside CONCATENATEX keeps the line chronological. The Y coordinate is inverted because SVG coordinates increase downward. A constant series sits at the middle; fewer than two observations return blank.
Missing dates are excluded, so the line connects across gaps. That is a deliberate limitation. If missing months must be visible, build separate line segments instead of silently converting missing values to zero. Each row also has its own minimum and maximum: compare trend shape, not vertical magnitude between sites.

Step 5: Make Power BI Render the Measures
Select each SVG measure and set its Data category to Image URL in the measure properties or Measure tools. Add DimSite[Site], Actual, Target, Attainment, Status Label and the SVG measures to a Table visual. Start here before adapting the layout to a Matrix.
Adjust the visual’s image-size settings and column widths until the icons and labels are readable. Formatting labels vary by visual and Desktop version. Use restrained row padding, consistent image dimensions, and a short legend explaining the target marker and sparkline scale.
Practical Example
Use these invented values for one selected month. They are teaching data, not customer results.
Site | Actual | Target | Attainment | Expected status |
North | 108000 | 100000 | 108% | On target |
Central | 95000 | 100000 | 95% | Watch |
South | 76000 | 100000 | 76% | Below target |
West | 135000 | 100000 | 135% | On target; overflow marker |
Enter several monthly rows per site to test the sparkline. Select one month to check the target indicators, then select several months to inspect the trend. Actual and Target aggregate over that selection; attainment is the ratio of their sums, not the average of monthly percentages.
Test a zero Actual, a blank Actual, a zero Target, one observation, a flat series, and a site-filtered grand total. Zero revenue should remain visible as valid underperformance when its target is positive. Missing revenue should not quietly become a red failure icon.
Bonus Tips and Common Mistakes
Pro Tip: Build a Small Visual System
Keep colors, dimensions and threshold rules consistent across your SVG measures. Separate numeric calculations from geometry, then assemble the XML last. This makes a later brand-color change manageable and helps colleagues review the business logic without reading every shape.
For a diverging variance bar, calculate Actual minus Target, place zero at the center, and draw negative values left and positive values right. A dumbbell joins Actual and Target with two circles. Both need an explicit shared scale when readers compare row lengths.
Common Mistake: Locale and Text Encoding
Use FORMAT with an explicit en-US locale for decimal coordinates. A decimal comma can corrupt an SVG attribute or point pair. The measures encode hash characters as %23 for the data URI. This is not a complete encoder for arbitrary user text. If you add dynamic names inside XML, escape XML-sensitive characters such as ampersands and angle brackets, and handle reserved URI characters deliberately.
Good to Know: Images Have Limits
Shapes inside an SVG image are not independent Power BI data points. Don’t promise separate click-to-filter behavior or shape-specific native tooltips. Keep numeric and text columns available for accessibility, sorting and exported data; an embedded SVG title alone is not a complete accessibility solution.
Test your actual report in Desktop, Service, mobile and the export formats your audience uses. Don’t assume identical rendering everywhere. In DirectQuery, iterating dates can increase source-query work; SVG text does not remove the cost of the measures underneath. Use Performance Analyzer to compare a representative page with and without the SVG columns, rather than inventing a universal row limit.
Final Thoughts
Start with one status icon and confirm it tells the same story as your numeric measures. Add a target bar when distance from plan matters, and a sparkline when the history changes the decision. Clear scales, sensible totals and honest treatment of missing data make the result useful.
The templates were reviewed and their SVG geometry rendered separately for this article. They were not executed in a live Power BI model here; validate the DAX and rendering in your own model before using them in production.
Sources and Further Reading
Continue Learning
Need help turning your measures into clearer reports? Explore Excelized’s Power BI consulting services.




Comments