DAX SVG measure · Progress to target
Power BI bullet chart with DAX (SVG measure)
By Nawaf Sharaf · Founder & Reliability Engineer · Updated
Use it when
- Each row has an actual and a target, and the distance to target matters.
- You are replacing a page of gauges: bullets compare across rows, gauges don't.
- Qualitative bands (poor/ok/good) are defined relative to the target.
Skip it when
Rows need to be compared on one absolute scale: each bullet is scaled to its own target. Use the data bar with target instead.
Three looks
The bullet chart in three palettes; click one to edit it.
Data it needs
The builder asks for exactly these fields; type your own measure and column names there.
- Actual measure
- The achieved value, e.g. [Sales].
- Target measure
- The goal for the same filter context, e.g. [Sales Target]. Bands are multiples of it.
The full DAX
Swap [Sales] and [Sales Target] for your own names.
Bullet =
-- VizForge SVG measure: bullet chart (actual bar, target tick, qualitative bands).
-- Set Measure tools > Data category = Image URL, then size it under Format > Visual > Image size.
-- Made with the free VizForge SVG measure builder: vizforge.ai/tools/svg-measure-builder
-- Need per-point tooltips, click-to-select inside the chart or Format-pane settings? Generate it as a real custom visual: vizforge.ai/visuals
VAR _Actual = [Sales]
VAR _Target = [Sales Target]
VAR _Max = MAX ( _Target * 1.3, _Actual )
RETURN
IF (
ISBLANK ( _Actual ),
BLANK (),
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='24' viewBox='0 0 160 24'><rect x='"
& FORMAT ( DIVIDE ( 0, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX ( 0, DIVIDE ( _Target * 0.6, _Max, 0 ) * 160 - DIVIDE ( 0, _Max, 0 ) * 160 ),
"0.0",
"en-US"
)
& "' height='24' fill='%23252423' fill-opacity='0.24'/><rect x='"
& FORMAT ( DIVIDE ( _Target * 0.6, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX (
0,
DIVIDE ( _Target * 0.9, _Max, 0 ) * 160 - DIVIDE ( _Target * 0.6, _Max, 0 ) * 160
),
"0.0",
"en-US"
)
& "' height='24' fill='%23252423' fill-opacity='0.15'/><rect x='"
& FORMAT ( DIVIDE ( _Target * 0.9, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX ( 0, DIVIDE ( _Max, _Max, 0 ) * 160 - DIVIDE ( _Target * 0.9, _Max, 0 ) * 160 ),
"0.0",
"en-US"
)
& "' height='24' fill='%23252423' fill-opacity='0.07'/><rect x='0' y='7.5' width='"
& FORMAT ( DIVIDE ( MAX ( 0, _Actual ), _Max, 0 ) * 160, "0.0", "en-US" )
& "' height='9' fill='%23118DFF'/><rect x='"
& FORMAT ( MIN ( DIVIDE ( _Target, _Max, 0 ) * 160, 159 ) - 1, "0.0", "en-US" )
& "' y='3' width='2' height='18' fill='%23252423'/></svg>"
)DAX user-defined function variant (VizForge.SvgBullet)
One-line call: Bullet = VizForge.SvgBullet ( [Sales], [Sales Target] )
-- 1) Paste into DAX query view and choose 'Update model with changes'.
-- Needs DAX user-defined functions (generally available since the June 2026 Power BI release).
DEFINE
/// Bullet chart as an SVG image URL (VizForge). Optional parameters default to the colours you picked.
FUNCTION VizForge.SvgBullet = (
pActual : NUMERIC,
pTarget : NUMERIC,
pMaxPct : NUMERIC = 1.3,
pPoorPct : NUMERIC = 0.6,
pBandColor : STRING = "#252423",
pOkPct : NUMERIC = 0.9,
pBarColor : STRING = "#118DFF",
pTargetColor : STRING = "#252423"
) =>
VAR _Actual = pActual
VAR _Target = pTarget
VAR _Max = MAX ( _Target * pMaxPct, _Actual )
RETURN
IF (
ISBLANK ( _Actual ),
BLANK (),
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='24' viewBox='0 0 160 24'><rect x='"
& FORMAT ( DIVIDE ( 0, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX ( 0, DIVIDE ( _Target * pPoorPct, _Max, 0 ) * 160 - DIVIDE ( 0, _Max, 0 ) * 160 ),
"0.0",
"en-US"
)
& "' height='24' fill='"
& SUBSTITUTE ( pBandColor, "#", "%23" )
& "' fill-opacity='0.24'/><rect x='"
& FORMAT ( DIVIDE ( _Target * pPoorPct, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX (
0,
DIVIDE ( _Target * pOkPct, _Max, 0 ) * 160 - DIVIDE ( _Target * pPoorPct, _Max, 0 ) * 160
),
"0.0",
"en-US"
)
& "' height='24' fill='"
& SUBSTITUTE ( pBandColor, "#", "%23" )
& "' fill-opacity='0.15'/><rect x='"
& FORMAT ( DIVIDE ( _Target * pOkPct, _Max, 0 ) * 160, "0.0", "en-US" )
& "' y='0' width='"
& FORMAT (
MAX ( 0, DIVIDE ( _Max, _Max, 0 ) * 160 - DIVIDE ( _Target * pOkPct, _Max, 0 ) * 160 ),
"0.0",
"en-US"
)
& "' height='24' fill='"
& SUBSTITUTE ( pBandColor, "#", "%23" )
& "' fill-opacity='0.07'/><rect x='0' y='7.5' width='"
& FORMAT ( DIVIDE ( MAX ( 0, _Actual ), _Max, 0 ) * 160, "0.0", "en-US" )
& "' height='9' fill='"
& SUBSTITUTE ( pBarColor, "#", "%23" )
& "'/><rect x='"
& FORMAT ( MIN ( DIVIDE ( _Target, _Max, 0 ) * 160, 159 ) - 1, "0.0", "en-US" )
& "' y='3' width='2' height='18' fill='"
& SUBSTITUTE ( pTargetColor, "#", "%23" )
& "'/></svg>"
)
EVALUATE { VizForge.SvgBullet ( [Sales], [Sales Target] ) }
-- 2) Then create the measure and set Measure tools > Data category = Image URL:
-- Bullet = VizForge.SvgBullet ( [Sales], [Sales Target] )
-- Colours and thresholds are optional parameters: pass a value after the fields to override one.
-- Pass colours as plain "#RRGGBB"; the function encodes the # for the data URI.Placing it in a report
Two measures per row, [Sales] and [Sales Target]; Image size 24 × 160. Because each row scales to 130% of its own target, it also works as a Card image for a single KPI. Remember the Image URL data category. All setup steps.
Gotchas
- Bands are drawn as tints of one colour, so they survive greyscale printing; don't use red/amber/green bands and a coloured bar together.
- If the actual exceeds the scale end, the scale stretches to the actual so the bar never overflows.
- A blank target collapses the bands to zero width: return BLANK() from the target only when the row should be empty too.
- Length. 492 characters whatever the data; no iteration.
- Context. Actual and target are both read in the row's filter context and the bands sit at 60% and 90% of that row's target. Bars in different rows are not on one scale: a long bar means close to target, not big.
- Tooltip. Hovering shows the SVG text; a blank report-page tooltip hides it.
When an SVG measure isn't enough
A bullet chart visual adds tooltips with actual, target and gap, and a format pane to tune bands without editing DAX. See the Bullet chart custom visual for what an interactive version can do.
KPI card designs using it
Finished designs built on this pattern, from the KPI card gallery.