DAX SVG measure · Comparison

Dumbbell chart in a Power BI table (DAX SVG)

A dumbbell shows two values per row and the gap between them: before and after, plan and actual, men and women. Unlike a variance bar it keeps the level visible, so a big change on a small base and a small change on a big base both read correctly.

By · Founder & Reliability Engineer · Updated

Dumbbell (before / after) rendered from the DAX below with sample data
Rendered from the measure below with sample data at 2× (160×16 px in Power BI). 313 characters.

Use it when

  • Comparing two periods or two groups per row where the level matters.
  • You want direction (dot colour) and size of change at once.

Skip it when

More than two values per row: a dumbbell becomes a dot plot. Use a column sparkline or a custom dot plot.

Three looks

The dumbbell (before / after) 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.

Row column
The column on the table's rows.
From / To measures
Two measures in the same unit, e.g. [Sales PY] and [Sales].

The full DAX

Swap 'Region'[Region] and [Sales PY] and [Sales] for your own names.

Dumbbell (measure)
Dumbbell =
-- VizForge SVG measure: dumbbell, scaled across every row in the visual.
-- Set Measure tools > Data category = Image URL. The row column must be the column on the table's rows.
-- 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 _From = [Sales PY]
VAR _To = [Sales]
VAR _Rows = ADDCOLUMNS ( ALLSELECTED ( 'Region'[Region] ), "@f", [Sales PY], "@t", [Sales] )
VAR _Lo = MINX ( _Rows, MIN ( [@f], [@t] ) )
VAR _Hi = MAXX ( _Rows, MAX ( [@f], [@t] ) )
RETURN
    IF (
        ISBLANK ( _To ),
        BLANK (),
        "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='16' viewBox='0 0 160 16'><line x1='"
            & FORMAT ( 5.5 + DIVIDE ( _From - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
            & "' y1='8' x2='"
            & FORMAT ( 5.5 + DIVIDE ( _To - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
            & "' y2='8' stroke-width='2.6999999999999997' stroke='%23E6E6E6'/><circle cx='"
            & FORMAT ( 5.5 + DIVIDE ( _From - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
            & "' cy='8' r='4.5' fill='%23605E5C'/><circle cx='"
            & FORMAT ( 5.5 + DIVIDE ( _To - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
            & "' cy='8' r='4.5' fill='"
            & IF ( _To < _From, "%23D64554", "%231AAB40" )
            & "'/></svg>"
    )
DAX user-defined function variant (VizForge.SvgDumbbell)

One-line call: Dumbbell = VizForge.SvgDumbbell ( 'Region'[Region], [Sales PY], [Sales] )

DAX query view script
-- 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
    /// Dumbbell (before / after) as an SVG image URL (VizForge). Optional parameters default to the colours you picked.
    FUNCTION VizForge.SvgDumbbell = (
            pCategory : ANYREF EXPR,
            pFrom : ANYREF EXPR,
            pTo : ANYREF EXPR,
            pLineColor : STRING = "#E6E6E6",
            pFromColor : STRING = "#605E5C",
            pDownColor : STRING = "#D64554",
            pUpColor : STRING = "#1AAB40"
        ) =>
        VAR _From = pFrom
        VAR _To = pTo
        VAR _Rows = ADDCOLUMNS ( ALLSELECTED ( pCategory ), "@f", CALCULATE ( pFrom ), "@t", CALCULATE ( pTo ) )
        VAR _Lo = MINX ( _Rows, MIN ( [@f], [@t] ) )
        VAR _Hi = MAXX ( _Rows, MAX ( [@f], [@t] ) )
        RETURN
            IF (
                ISBLANK ( _To ),
                BLANK (),
                "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='16' viewBox='0 0 160 16'><line x1='"
                    & FORMAT ( 5.5 + DIVIDE ( _From - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
                    & "' y1='8' x2='"
                    & FORMAT ( 5.5 + DIVIDE ( _To - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
                    & "' y2='8' stroke-width='2.6999999999999997' stroke='"
                    & SUBSTITUTE ( pLineColor, "#", "%23" )
                    & "'/><circle cx='"
                    & FORMAT ( 5.5 + DIVIDE ( _From - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
                    & "' cy='8' r='4.5' fill='"
                    & SUBSTITUTE ( pFromColor, "#", "%23" )
                    & "'/><circle cx='"
                    & FORMAT ( 5.5 + DIVIDE ( _To - _Lo, _Hi - _Lo, 0.5 ) * 149, "0.0", "en-US" )
                    & "' cy='8' r='4.5' fill='"
                    & IF (
                        _To < _From,
                        SUBSTITUTE ( pDownColor, "#", "%23" ),
                        SUBSTITUTE ( pUpColor, "#", "%23" )
                    )
                    & "'/></svg>"
            )

EVALUATE { VizForge.SvgDumbbell ( 'Region'[Region], [Sales PY], [Sales] ) }

-- 2) Then create the measure and set Measure tools > Data category = Image URL:
-- Dumbbell = VizForge.SvgDumbbell ( 'Region'[Region], [Sales PY], [Sales] )
--    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

'Region'[Region] on rows, [Sales PY] and [Sales] as the two ends; 16 × 160. Sort the table by the change, not by the region name, so the long bars group together. Remember the Image URL data category. All setup steps.

Gotchas

When an SVG measure isn't enough

A dumbbell visual can sort by change, label both ends on hover and let people click one end to filter by that period. See the Tornado 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.