
Svg Visuals
- 38 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Generate inline SVG visualizations in Power BI via DAX measures with the ImageUrl data category, such as sparklines, bullet charts, and KPI indicators.
About
Generates SVG visuals through DAX measures and extension measures using the ImageUrl data category for inline graphics in PBIR reports. A developer uses it to build DAX-driven charts like progress bars, sparklines, KPI indicators, and IBCS bars.
- Builds inline SVG charts from DAX measures
- Uses ImageUrl data category and extension measures
Svg Visuals by the numbers
- 38 all-time installs (skills.sh)
- Ranked #1,016 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/data-goblin/power-bi-agentic-development --skill svg-visualsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 38 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Generate inline SVG visualizations in Power BI via DAX measures with the ImageUrl data category, such as sparklines, bullet charts, and KPI indicators.
Files
SVG Visuals via DAX Measures (PBIR)
Report modification requires tooling. Two paths exist:
1. `pbir` CLI (preferred) -- use thepbircommand and thepbir-cliskill. Install withuv tool install pbir-cliorpip install pbir-cli. Check availability withpbir --version.
2. Direct JSON modification -- ifpbiris not available, use thepbir-formatskill (pbip plugin) for PBIR JSON structure and patterns. Validate every change withjq empty <file.json>.
>
If neither thepbir-cliskill nor thepbir-formatskill is loaded, ask the user to install the appropriate plugin before proceeding with report modifications.
Generate inline SVG graphics using DAX measures that return SVG markup strings. These render as images in table, matrix, card, image, and slicer visuals. Store as extension measures in reportExtensions.json.
How It Works
1. A DAX measure returns an SVG string prefixed with data:image/svg+xml;utf8, 2. The measure's dataCategory is set to ImageUrl 3. Power BI renders the SVG as an image in supported visuals
Supported Visuals
- Table (
tableEx):grid.imageHeight/grid.imageWidth--references/svg-table-matrix.md - Matrix (
pivotTable): same as table --references/svg-table-matrix.md - Image (
image):sourceType='imageData'+sourceField--references/svg-image-visual.md - Card/New (
cardVisual):callout.imageFX--references/svg-card-slicer.md - Slicer/New (
advancedSlicerVisual): header images --references/svg-card-slicer.md
Workflow: Creating an SVG Measure
Step 0: Design and Preview
Before writing DAX, design the SVG visually:
1. Query the model first -- use DAX Studio or Tabular Editor CLI to get actual values with the intended filter context. Use real numbers, not placeholders. 2. Write static SVG to a temp file -- save to /tmp/mockup.svg and open it in a browser to preview layout, colors, and proportions. 3. Ask for feedback before converting to DAX -- iterating on static SVG is far easier than on DAX string concatenation. 4. Colors must be hex codes with `#` -- e.g., fill='#2B7A78'. Never use %23 URL encoding or named colors. Always hex.
Step 1: Create the Extension Measure
Create the extension measure in reportExtensions.json manually (see the pbir-format skill in the pbip plugin for JSON structure).
# Example using pbir_object_model (if available):
report.add_extension_measure(
table="Orders",
name="Sparkline SVG",
expression='''
VAR _Prefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 30'>"
VAR _Bar = "<rect x='0' y='0' width='50' height='30' fill='#2196F3'/>"
VAR _Suffix = "</svg>"
RETURN _Prefix & _Bar & _Suffix
''',
data_type="Text",
data_category="ImageUrl",
display_folder="SVG Charts",
)
report.save()Step 1b: Review
Before presenting the measure to the user, dispatch the svg-reviewer agent to validate syntax and provide design feedback.
Step 2: Bind to a Visual
Extension measures use "Schema": "extension" in the SourceRef:
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {"Schema": "extension", "Entity": "Orders"}
},
"Property": "Sparkline SVG"
}
}
}For image visuals, set sourceType='imageData' with sourceField in the visual.json (see references/svg-image-visual.md).
Step 3: Validate
Validate JSON syntax with jq empty <reportExtensions.json> and inspect the file to confirm measure definitions and data categories.
Prefer UDF Libraries Over Custom DAX
Before writing a custom SVG measure from scratch, check whether an existing UDF library already provides the chart type:
- PowerofBI.IBCS (Andrzej Leszkiewicz) -- IBCS-compliant bar, column, waterfall, pin, small multiples, and P&L charts. Preferred for business reporting with AC/PY/BU/FC comparisons. Install from https://daxlib.org/package/PowerofBI.IBCS/
- DaxLib.SVG (Jake Duddy) -- general-purpose sparklines, bars, boxplots, heatmaps, jitter, violin, progress bars, pills. Install from https://daxlib.org/package/DaxLib.SVG/ -- source at https://github.com/daxlib/dev-daxlib-svg
- PowerBI MacGuyver Toolbox (Stepan Resl / Data Goblins) -- C# scripts that generate SVG measures via Tabular Editor
To check if a library is installed, look for functions/measures starting with PowerofBI.IBCS., Viz., Compound., or Element.. Only write custom SVG DAX when no library function covers the required visualization. See references/community-examples.md for full function listings and additional libraries.
Installing UDF libraries
UDF libraries are installed into the semantic model, not the report. Use one of these tools:
- Tabular Editor CLI (
tecommand) -- use thete-docsskill for guidance - Power BI MCP server -- if available, use it to modify the model directly
- `connect-pbid` skill -- connect to Power BI Desktop's local Analysis Services instance via TOM/PowerShell
- `tmdl` skill -- edit TMDL files directly in a PBIP project (last resort)
DAX SVG Conventions
Measure Structure (VAR Pattern)
Every SVG measure must follow a strict VAR-based structure. Organize code into clearly separated regions:
SVG Measure =
-- CONFIG: Input fields and visual parameters
VAR _Actual = [Sales Amount]
VAR _Target = [Sales Target]
VAR _Scope = ALLSELECTED ( 'Product'[Category] )
-- CONFIG: Colors
VAR _BarColor = "#5B8DBE"
VAR _TargetColor = "#333333"
-- NORMALIZATION: Scale values to SVG coordinate space
VAR _AxisMax = CALCULATE( MAXX( _Scope, [Sales Amount] ), REMOVEFILTERS( 'Product'[Category] ) ) * 1.1
VAR _AxisRange = 100
VAR _ActualNormalized = DIVIDE( _Actual, _AxisMax ) * _AxisRange
-- SVG ELEMENTS: One VAR per visual element
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 25'>"
VAR _Sort = "<desc>" & FORMAT( _Actual, "000000000000" ) & "</desc>"
VAR _Bar = "<rect x='0' y='5' width='" & _ActualNormalized & "' height='15' fill='" & _BarColor & "'/>"
VAR _TargetLine = "<rect x='" & DIVIDE( _Target, _AxisMax ) * _AxisRange & "' y='2' width='2' height='21' fill='" & _TargetColor & "'/>"
VAR _SvgSuffix = "</svg>"
-- ASSEMBLY: Combine in rendering order (back to front)
VAR _SVG = _SvgPrefix & _Sort & _Bar & _TargetLine & _SvgSuffix
RETURN _SVGKey conventions:
- CONFIG section first -- input measures, scope column, colors, font settings. Users change only this section.
- NORMALIZATION section -- scale raw values to SVG coordinate space (see below)
- SVG ELEMENTS -- one VAR per
<rect>,<circle>,<text>,<line>, etc. - ASSEMBLY -- concatenate elements in document order (first = back layer, last = front)
- `<desc>` sort trick -- embed
FORMAT(_Actual, "000000000000")in a<desc>tag so the table/matrix can sort by the SVG column
Axis Normalization (Critical)
SVG coordinates must be normalized to a fixed range. Raw measure values (e.g., 1,234,567) cannot be used directly as pixel coordinates. The standard pattern:
-- 1. Define the SVG coordinate range
VAR _BarMin = 0 -- leftmost position (or offset for labels)
VAR _BarMax = 100 -- rightmost position
-- 2. Find the maximum value across all rows in the visual's filter context
VAR _Scope = ALLSELECTED( 'Table'[GroupColumn] )
VAR _MaxInScope = CALCULATE( MAXX( _Scope, [Measure] ), REMOVEFILTERS( 'Table'[GroupColumn] ) )
VAR _AxisMax = _MaxInScope * 1.1 -- 10% padding
-- 3. Normalize each value to the SVG range
VAR _AxisRange = _BarMax - _BarMin
VAR _Normalized = DIVIDE( _Actual, _AxisMax ) * _AxisRangeUse ALLSELECTED for the scope when the chart should respond to slicer context. Use ALL for a fixed axis across all filter contexts. The * 1.1 padding prevents bars from touching the edge.
HASONEVALUE Guard
Table/matrix SVG measures must guard against subtotal/total rows where multiple categories are in scope:
IF( HASONEVALUE( 'Table'[GroupColumn] ),
-- SVG code here
)Without this guard, the measure evaluates on grand total rows with meaningless aggregated values.
Escaping and Color Rules
- Single quotes for SVG attributes -- avoids DAX double-quote escaping:
fill='#2196F3' - Double quotes in DAX: escape as
""(DAX convention) - `viewBox` for responsive scaling:
viewBox='0 0 100 25' - `xmlns` required on
<svg>element - Hex colors with `#` only -- e.g.,
fill='#2196F3'.%23URL encoding causes errors in image visuals. Never use named colors. - No JavaScript -- SVG must be purely declarative
SVG Coordinate System
- Y=0 is at the top -- invert values for charts:
_Height - _Value - Use
viewBoxwith a 0-100 range for normalized coordinates - Elements render in document order (first = back, last = front)
CONCATENATEX for Series Data
For sparklines and multi-point charts, build coordinate strings with CONCATENATEX:
VAR _Points = CONCATENATEX(
_SparklineTable,
[X] & "," & (100 - [Y]),
" ",
[Date], ASC
)
-- Produces: "0,80 10,60 20,40 30,20"
-- Use in: <polyline points='...'/>Best Practices
- Check UDF libraries first -- use DaxLib.SVG or MacGuyver Toolbox functions before writing custom DAX
- VAR pattern mandatory -- one VAR per config value, one VAR per SVG element, assembly at the end
- Normalize all values -- raw measure values must be scaled to SVG coordinate range
- HASONEVALUE guard -- always guard against total/subtotal rows in table/matrix context; use
ISINSCOPEfor nested hierarchy levels <desc>sort trick -- embed formatted value in<desc>for sortable SVG columns- Use
viewBoxfor responsive scaling instead of fixed width/height - Round coordinates to integers for performance (shorter strings, cheaper FORMAT calls)
- Store as extension measures -- SVG measures don't belong in the semantic model
- Use
display_folderto organize SVG measures (e.g.,"SVG Charts") - Preview first -- save static SVG to
/tmp/, open in browser, iterate before writing DAX - 32K character limit on the rendered SVG string per cell (not the DAX expression); see
references/svg-table-matrix.mdfor diagnosis and mitigation - Pre-aggregate in model measures -- let the storage engine cache aggregations; the SVG measure maps numbers to coordinates only
- Hex colors only --
#directly, never%23URL encoding - Image visuals need no
queryblock -- onlyobjects.imagewithsourceType='imageData'andsourceField - Accessibility: every SVG encoding primary data needs adjacent readable columns and a dynamic alt-text measure; see
references/svg-accessibility.md
reportExtensions.json Format
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/reportExtension/1.0.0/schema.json",
"name": "extension",
"entities": [{
"name": "ExistingTable",
"measures": [{
"name": "Sparkline SVG",
"dataType": "Text",
"dataCategory": "ImageUrl",
"expression": "...",
"displayFolder": "SVG Charts"
}]
}]
}Limitations
- No interactivity -- SVG images are static (no hover, click, tooltip)
- No JavaScript -- inline scripts are stripped
- 32K character limit per rendered cell string (not the DAX expression);
CONCATENATEXover 30+ series points easily approaches this; prefer<polyline>over individual shapes, integer coordinates, and pre-aggregated series; seereferences/svg-table-matrix.mdfor full diagnosis - Per-cell formula-engine cost -- each visible cell evaluates the string-building expression; push aggregations into model measures so SVG assembly is coordinate-mapping only
- Accessibility gap -- screen readers receive no per-cell data from an SVG URI; mitigate with adjacent readable columns and dynamic alt text; see
references/svg-accessibility.md - Classic card (
card) does NOT support SVG -- usecardVisualinstead
When to Use SVG Measures
SVG measures are the preferred choice for simple inline graphics embedded in tables, matrices, cards, and image visuals. Use SVG when you need:
- Sparklines, data bars, progress bars, or status indicators inside table/matrix cells
- KPI micro-charts in card visuals
- Lightweight visuals that don't require interactivity or complex data transforms
- No additional custom visual registration (works with native visuals)
Use Deneb instead for complex, interactive visualizations (cross-filtering, tooltips, hover states) or chart types that require extensive data transforms. Use Python/R instead for statistical analysis charts (distributions, regressions, correlations).
References
Community Examples and Libraries
- `references/community-examples.md` -- Community SVG templates organized by target visual type (Table/Matrix, Image, Card), including DaxLib.SVG functions, Kerry Kolosko templates, and PowerBI MacGuyver Toolbox patterns
By Visual Type
- `references/svg-table-matrix.md` -- Patterns for Table/Matrix: data bar, bullet chart, dumbbell, overlapping bars, lollipop, status pill, sparkline, bar sparkline, area sparkline, UDF patterns; axis normalization, sort trick, image size configuration, and per-cell performance guidance
- `references/svg-image-visual.md` -- Patterns for Image visuals: KPI header, sparkline with endpoint, dashboard tile; sourceType binding, dynamic/conditional layout, responsive width guidance
- `references/svg-card-slicer.md` -- Patterns for Card/Slicer: arrow indicator, mini gauge, mini donut, progress bar, narrative sentence; card binding via
callout.imageFX
Accessibility
- `references/svg-accessibility.md` -- Accessibility for SVG measures: adjacent readable columns, dynamic alt text, color-only encoding, contrast requirements, and severity guidance for audit findings
General
- `references/svg-elements.md` -- SVG element reference (rect, circle, line, polyline, text, path, gradient, group)
Examples
Ready-to-use DAX measure expressions in examples/:
- `sparkline-measure.dax` -- Line sparkline (polyline + CONCATENATEX)
- `progress-bar-measure.dax` -- Conditional progress bar
- `dumbbell-chart-measure.dax` -- Actual vs target dumbbell
- `bullet-chart-measure.dax` -- Bullet chart with sentiment action dots
- `overlapping-bars-measure.dax` -- Overlapping bars with variance label
- `boxplot-measure.dax` -- Box-and-whisker plot (inspired by DaxLib.SVG)
- `ibcs-bar-measure.dax` -- IBCS-compliant horizontal bar (inspired by avatorl)
- `jitter-plot-measure.dax` -- Dot strip chart with jitter (inspired by DaxLib.SVG)
- `overlapping-bars-with-variance-measure.dax` -- Overlapping bars with variance bar + arrow icon + % label (Kurt Buhler / Data Goblins)
- `lollipop-conditional-measure.dax` -- Lollipop with scaled dot + auto-formatted label (Kurt Buhler / Data Goblins)
- `waterfall-measure.dax` -- Waterfall with cumulative OFFSET positioning + connector lines (Kurt Buhler / Data Goblins)
- `status-pill-measure.dax` -- Rounded pill badge with category color + text label (Kurt Buhler / Data Goblins)
Helper Libraries
| Library | Author | Key Features |
|---|---|---|
| DaxLib.SVG | Jake Duddy | UDF library: area, line, boxplot, heatmap, jitter, violin |
| PBI-Core-Visuals-SVG-HTML | David Bacci | Chips, tornado, gradient matrix, bar UDF |
| PowerBI MacGuyver Toolbox | Stepan Resl / Data Goblins | 20+ bar, 14+ line, 24+ KPI templates |
| Dashboard Design UDF Library | Dashboard-Design | Target line bars, pill visuals |
| Kerry Kolosko Templates | Kerry Kolosko | Sparklines, data bars, KPI cards |
Related Skills
- `pbi-report-design` -- Layout and design best practices
- `deneb-visuals` -- Vega/Vega-Lite for complex interactive visualizations
- `python-visuals` -- matplotlib/seaborn for statistical charts
- `r-visuals` -- ggplot2 for statistical charts
- `pbir-format` (pbip plugin) -- PBIR JSON format reference (extension measures, ImageUrl binding)
-- BoxPlot SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Horizontal box-and-whisker plot showing Q1, median, Q3, and 1.5*IQR whiskers
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 100)
-- Inspired by: DaxLib.SVG (EvaluationContext/daxlib.svg, MIT License, James Featherstone)
BoxPlot SVG =
VAR _Scope = ALLSELECTED('Product'[Category])
VAR _Data = ADDCOLUMNS(_Scope, "@Val", [Sales Amount])
-- Quartile statistics
VAR _Q1 = PERCENTILEX.INC(_Data, [@Val], 0.25)
VAR _Median = PERCENTILEX.INC(_Data, [@Val], 0.5)
VAR _Q3 = PERCENTILEX.INC(_Data, [@Val], 0.75)
VAR _IQR = _Q3 - _Q1
-- Whiskers (clamped to 1.5 * IQR from box edges)
VAR _WhiskerLow = MAX(MINX(_Data, [@Val]), _Q1 - 1.5 * _IQR)
VAR _WhiskerHigh = MIN(MAXX(_Data, [@Val]), _Q3 + 1.5 * _IQR)
-- Normalize to SVG coordinates
VAR _DataMin = MINX(_Data, [@Val])
VAR _DataMax = MAXX(_Data, [@Val])
VAR _Range = _DataMax - _DataMin
VAR _W = 96
VAR _H = 22
VAR _Pad = 2
VAR _BoxH = _H * 0.5
VAR _BoxY = (_H - _BoxH) / 2
VAR _MidY = _H / 2
-- Normalize function: value -> x position
VAR _Scale = DIVIDE(_W - 2 * _Pad, _Range)
VAR _Q1x = (_Q1 - _DataMin) * _Scale + _Pad
VAR _Q3x = (_Q3 - _DataMin) * _Scale + _Pad
VAR _Medx = (_Median - _DataMin) * _Scale + _Pad
VAR _WLx = (_WhiskerLow - _DataMin) * _Scale + _Pad
VAR _WHx = (_WhiskerHigh - _DataMin) * _Scale + _Pad
-- Colors
VAR _BoxFill = "#E8F0FE"
VAR _BoxStroke = "#448FD6"
VAR _WhiskerColor = "#999999"
VAR _MedianColor = "#D64444"
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 " & _H & "'>"
-- Whisker lines (thin horizontal)
VAR _LowerWhisker = "<line x1='" & _WLx & "' y1='" & _MidY & "' x2='" & _Q1x & "' y2='" & _MidY & "' stroke='" & _WhiskerColor & "' stroke-width='1'/>"
VAR _UpperWhisker = "<line x1='" & _Q3x & "' y1='" & _MidY & "' x2='" & _WHx & "' y2='" & _MidY & "' stroke='" & _WhiskerColor & "' stroke-width='1'/>"
-- Whisker caps (vertical)
VAR _LowerCap = "<line x1='" & _WLx & "' y1='" & _BoxY & "' x2='" & _WLx & "' y2='" & (_BoxY + _BoxH) & "' stroke='" & _WhiskerColor & "' stroke-width='1'/>"
VAR _UpperCap = "<line x1='" & _WHx & "' y1='" & _BoxY & "' x2='" & _WHx & "' y2='" & (_BoxY + _BoxH) & "' stroke='" & _WhiskerColor & "' stroke-width='1'/>"
-- Box (Q1 to Q3)
VAR _Box = "<rect x='" & _Q1x & "' y='" & _BoxY & "' width='" & (_Q3x - _Q1x) & "' height='" & _BoxH & "' fill='" & _BoxFill & "' stroke='" & _BoxStroke & "' stroke-width='1.5'/>"
-- Median line (vertical, red)
VAR _MedianLine = "<line x1='" & _Medx & "' y1='" & _BoxY & "' x2='" & _Medx & "' y2='" & (_BoxY + _BoxH) & "' stroke='" & _MedianColor & "' stroke-width='2'/>"
VAR _SvgSuffix = "</svg>"
RETURN
IF(HASONEVALUE('Product'[Category]),
_SvgPrefix & _LowerWhisker & _UpperWhisker & _LowerCap & _UpperCap & _Box & _MedianLine & _SvgSuffix,
BLANK()
)
-- Bullet Chart with Action Dots SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Shows actual bar, target line, and sentiment-colored dot
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 100)
-- Source: SpaceParts production model
SVG Bullet Chart =
-- Config
VAR _Actual = [MTD Turnover]
VAR _Target = [MTD Turnover 1YP]
VAR _Performance = DIVIDE(_Actual - _Target, _Target)
-- Sentiment thresholds
VAR _VeryBad = -0.05
VAR _Bad = -0.025
VAR _Good = 0.025
VAR _VeryGood = 0.05
-- Chart dimensions
VAR _BarMax = 100
VAR _BarMin = 20
VAR _Scope = ALL('Customers'[Key Account Name])
-- Colors
VAR _BackgroundColor = "#F5F5F5"
VAR _BarFillColor = "#CFCFCF"
VAR _BaselineColor = "#737373"
VAR _TargetColor = "#000000"
VAR _ActionDotFill =
SWITCH(TRUE(),
_Performance < _VeryBad, "#f4ae4c",
_Performance < _Bad, "#ffe075",
_Performance > _VeryGood, "#2D6390",
_Performance > _Good, "#74B2FF",
"#FFFFFF00"
)
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [MTD Turnover]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [MTD Turnover 1YP]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([MTD Turnover], [MTD Turnover 1YP]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = (DIVIDE(_Target, _AxisMax) * _AxisRange) + _BarMin - 1
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _ActionDot = "<circle cx='10' cy='11' r='5' fill='" & _ActionDotFill & "'/>"
VAR _BarBg = "<rect x='" & _BarMin & "' y='2' width='" & _BarMax & "' height='80%' fill='" & _BackgroundColor & "'/>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='5' width='" & _ActualNormalized & "' height='50%' fill='" & _BarFillColor & "'/>"
VAR _Baseline = "<rect x='" & _BarMin & "' y='4' width='1' height='60%' fill='" & _BaselineColor & "'/>"
VAR _TargetLine = "<rect x='" & _TargetNormalized & "' y='2' width='2' height='80%' fill='" & _TargetColor & "'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _ActionDot & _BarBg & _ActualBar & _Baseline & _TargetLine & _SvgSuffix
-- Dumbbell Chart SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Compares actual vs target as two connected circles
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 100)
-- Source: SpaceParts production model
SVG Dumbbell Chart =
-- Config
VAR _Actual = [Actuals MTD]
VAR _Target = [Sales Target MTD]
-- Chart dimensions
VAR _SvgWidth = 100
VAR _SvgHeight = 25
VAR _Scope = ALLSELECTED('Customers'[Key Account Name])
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [Actuals MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([Actuals MTD], [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _SvgWidth
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = DIVIDE(_Target, _AxisMax) * _AxisRange
-- Colors (blue if on target, red if off)
VAR _AxisColor = "#C7C7C7"
VAR _Fill = IF(_Actual > _Target, "#448FD6", "#D64444")
VAR _Stroke = IF(_Actual > _Target, "#2F6698", "#982F2F")
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg width='" & _SvgWidth & "' height='" & _SvgHeight & "' xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _Axis = "<line x1='0' y1='" & _SvgHeight / 2 & "' x2='" & _SvgWidth & "' y2='" & _SvgHeight / 2 & "' stroke='" & _AxisColor & "'/>"
VAR _Origin = "<circle cx='2' cy='" & _SvgHeight / 2 & "' r='2' fill='" & _AxisColor & "'/>"
VAR _DumbbellLine = "<line x1='" & _ActualNormalized & "' y1='" & _SvgHeight / 2 & "' x2='" & _TargetNormalized & "' y2='" & _SvgHeight / 2 & "' stroke='" & _Fill & "' stroke-width='3'/>"
VAR _TargetCircle = "<circle cx='" & _TargetNormalized & "' cy='" & _SvgHeight / 2 & "' r='4' fill='#F5F5F5' stroke='#C7C7C7' stroke-width='1.5'/>"
VAR _ActualCircle = "<circle cx='" & _ActualNormalized & "' cy='" & _SvgHeight / 2 & "' r='4' fill='" & _Fill & "' stroke='" & _Stroke & "' stroke-width='1.5'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _Axis & _Origin & _DumbbellLine & _TargetCircle & _ActualCircle & _SvgSuffix
-- IBCS Absolute Values Bar Chart SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Horizontal bar showing AC (actuals) vs PY (previous year) per IBCS standards
-- AC = solid dark bar, PY = grey outlined bar, delta shown as colored extension
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 150)
-- Inspired by: avatorl/dax-udf-svg-ibcs (Andrzej Leszkiewicz, MIT License, powerofbi.org)
-- Reference: International Business Communication Standards (IBCS)
IBCS Bar Chart =
-- Config
VAR _AC = [Actuals MTD]
VAR _PY = [Sales Target MTD]
VAR _Delta = _AC - _PY
-- Chart dimensions
VAR _BarMax = 110
VAR _BarMin = 40
VAR _H = 22
VAR _BarH = 10
VAR _BarY = (_H - _BarH) / 2
VAR _Scope = ALLSELECTED('Customers'[Key Account Name])
-- IBCS colors
VAR _ACColor = "#404040"
VAR _PYColor = "#C6C6C6"
VAR _PosColor = "#006600"
VAR _NegColor = "#CC0000"
VAR _DeltaColor = IF(_Delta >= 0, _PosColor, _NegColor)
-- Axis normalization
VAR _MaxAC = CALCULATE(MAXX(_Scope, [Actuals MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxPY = CALCULATE(MAXX(_Scope, [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxAC, _MaxPY),
CALCULATE(MAX([Actuals MTD], [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ACw = DIVIDE(_AC, _AxisMax) * _AxisRange
VAR _PYw = DIVIDE(_PY, _AxisMax) * _AxisRange
-- Label formatting
VAR _DeltaLabel = FORMAT(_Delta, "+#,0;-#,0;0")
VAR _ACLabel = SWITCH(TRUE(),
_AC <= 1E3, FORMAT(_AC, "#,0"),
_AC <= 1E6, FORMAT(_AC, "#,0, K"),
FORMAT(_AC, "#,0,, M")
)
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 " & _H & "'>"
VAR _Sort = "<desc>" & FORMAT(_AC, "000000000000") & "</desc>"
-- PY bar (grey, outlined per IBCS)
VAR _PYBar = "<rect x='" & _BarMin & "' y='" & (_BarY - 1) & "' width='" & _PYw & "' height='" & (_BarH + 2) & "' fill='none' stroke='" & _PYColor & "' stroke-width='1'/>"
-- AC bar (solid dark, narrower to sit inside PY)
VAR _ACBar = "<rect x='" & _BarMin & "' y='" & _BarY & "' width='" & _ACw & "' height='" & _BarH & "' fill='" & _ACColor & "'/>"
-- Delta extension (colored bar from PY end to AC end)
VAR _DeltaX = _BarMin + MIN(_ACw, _PYw)
VAR _DeltaW = ABS(_ACw - _PYw)
VAR _DeltaBar = "<rect x='" & _DeltaX & "' y='" & (_BarY + 2) & "' width='" & _DeltaW & "' height='" & (_BarH - 4) & "' fill='" & _DeltaColor & "'/>"
-- Labels
VAR _ValueLabel = "<text x='" & _BarMin - 3 & "' y='" & (_H / 2 + 4) & "' font-family='Segoe UI' font-size='9' font-weight='600' text-anchor='end' fill='#333333'>" & _ACLabel & "</text>"
VAR _DeltaLabelSvg = "<text x='" & (_BarMin + MAX(_ACw, _PYw) + 3) & "' y='" & (_H / 2 + 4) & "' font-family='Segoe UI' font-size='8' font-weight='600' fill='" & _DeltaColor & "'>" & _DeltaLabel & "</text>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _ValueLabel & _PYBar & _ACBar & _DeltaBar & _DeltaLabelSvg & _SvgSuffix
-- Jitter Plot SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Scatter-like dot plot with pseudo-random vertical jitter per data point
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 100)
-- Inspired by: DaxLib.SVG (EvaluationContext/daxlib.svg, MIT License, James Featherstone)
-- Also: Kerry Kolosko jitter plot template (powerbi-macguyver-toolbox)
-- Warning: limited to ~30 data points due to 32K SVG string limit
Jitter Plot SVG =
VAR _Scope = ALLSELECTED('Product'[Category])
VAR _W = 96
VAR _H = 22
VAR _Pad = 4
VAR _DotR = 2.5
VAR _Color = "#448FD6"
VAR _AvgColor = "#D64444"
-- Get data points
VAR _Data = ADDCOLUMNS(
_Scope,
"@Val", [Sales Amount],
"@Idx", RANKX(_Scope, [Sales Amount], , ASC)
)
VAR _Min = MINX(_Data, [@Val])
VAR _Max = MAXX(_Data, [@Val])
VAR _Avg = AVERAGEX(_Data, [@Val])
VAR _Range = _Max - _Min
VAR _Scale = DIVIDE(_W - 2 * _Pad, _Range)
-- Normalize average position
VAR _AvgX = (_Avg - _Min) * _Scale + _Pad
-- Build dots with pseudo-random Y jitter
-- Jitter seed: deterministic from value and index to avoid RAND() recalc issues
VAR _Dots = CONCATENATEX(
_Data,
VAR _X = ([@Val] - _Min) * _Scale + _Pad
VAR _Seed = ABS([@Val] * 12345 + [@Idx] * 67890)
VAR _Jitter = MOD(_Seed, 10000) / 10000
VAR _Y = _Pad + _Jitter * (_H - 2 * _Pad)
RETURN "<circle cx='" & _X & "' cy='" & _Y & "' r='" & _DotR & "' fill='" & _Color & "' opacity='0.6'/>",
"",
[@Idx], ASC
)
-- Average line (vertical)
VAR _AvgLine = "<line x1='" & _AvgX & "' y1='1' x2='" & _AvgX & "' y2='" & (_H - 1) & "' stroke='" & _AvgColor & "' stroke-width='1.5' stroke-dasharray='3,2'/>"
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 " & _H & "'>"
VAR _SvgSuffix = "</svg>"
RETURN
IF(HASONEVALUE('Product'[Category]),
_SvgPrefix & _Dots & _AvgLine & _SvgSuffix,
BLANK()
)
-- SVG Lollipop Chart with Conditional Formatting
-- Author: Kurt Buhler / Data Goblins (PowerBI MacGuyver Toolbox)
-- Target: Table / Matrix (Image size: H25 x W100)
-- Placeholders: Replace __ACTUAL_MEASURE, __TARGET_MEASURE, __GROUPBY_COLUMN with your fields
-- Input field config
VAR _Actual = __ACTUAL_MEASURE
VAR _Target = __TARGET_MEASURE
VAR _Performance = DIVIDE ( _Actual - _Target, _Target )
-- Font config
VAR _LabelFont = "Segoe UI"
VAR _LabelWeight = "600"
VAR _LabelSize = "11"
-- Conditional colors
VAR _BarColor =
SWITCH (
TRUE(),
_Performance < 0, "#ffd43b",
_Performance > 0, "#a5d8ff",
"#CACACA"
)
VAR _LabelColor =
SWITCH (
TRUE(),
_Performance < 0, "#c68c03",
_Performance > 0, "#1971c2",
"#CACACA"
)
-- Number formatting
VAR _NumberFormat =
SWITCH (
TRUE (),
_Actual <= 1E3, FORMAT ( _Actual, "#,0." ),
_Actual <= 1E6, FORMAT ( _Actual, "#,0,.0 K" ),
_Actual <= 1E9, FORMAT ( _Actual, "#,0,,.0 M" ),
_Actual <= 1E12, FORMAT ( _Actual, "#,0,,,.0 bn" )
)
-- Chart Config
VAR _BarMax = 95
VAR _BarMin = 44
VAR _Scope = ALLSELECTED ( __GROUPBY_COLUMN )
-- Axis normalization
VAR _MaxActualsInScope =
CALCULATE( MAXX( _Scope, __ACTUAL_MEASURE ), REMOVEFILTERS( __GROUPBY_COLUMN ) )
VAR _AxisMax = IF ( HASONEVALUE ( __GROUPBY_COLUMN ), _MaxActualsInScope ) * 1.1
VAR _DotSizeMin = IF ( HASONEVALUE ( __GROUPBY_COLUMN ), 3.5 )
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE ( _Actual, _AxisMax ) * _AxisRange
-- SVG elements
VAR _SvgPrefix = "data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT ( _Actual, "000000000000" ) & "</desc>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='10' width='" & _ActualNormalized & "' height='15%' fill='" & _BarColor & "'/>"
VAR _ActualDot = "<circle cx='" & _ActualNormalized + _BarMin & "' cy='11.75' r='" & MAX ( DIVIDE ( _Actual, _AxisMax ) * 7.5, _DotSizeMin ) & "' fill='" & _BarColor &"'/>"
VAR _ActualLabel = "<text x='40' y='16' font-family='" & _LabelFont & "' font-size='" & _LabelSize & "' font-weight='" & _LabelWeight & "' text-anchor='end' fill='" & _LabelColor & "'>" & _NumberFormat & "</text>"
VAR _SvgSuffix = "</svg>"
VAR _SVG =
_SvgPrefix & _Sort
& _ActualBar & _ActualDot & _ActualLabel
& _SvgSuffix
RETURN _SVG
-- Overlapping Bars with Variance SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Two bars (actual/target) with variance indicator and label
-- Target visual: Table or Matrix (imageHeight: 25, imageWidth: 100)
-- Source: SpaceParts production model
SVG Overlapping Bars =
-- Config
VAR _Actual = [Actuals MTD]
VAR _Target = [Budget MTD]
VAR _Performance = DIVIDE(_Actual - _Target, _Target)
-- Font
VAR _Font = "Segoe UI"
VAR _FontSize = 10
VAR _FontWeight = 600
-- Chart dimensions
VAR _BarMax = 100
VAR _BarMin = 30
VAR _Scope = ALLSELECTED('Customers'[Key Account Name])
-- Colors
VAR _ActualColor = "#686868"
VAR _TargetColor = "#e1dfdd"
VAR _VarianceColor = IF(_Performance < 0, "#fab005", "#2094ff")
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [Actuals MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [Budget MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([Actuals MTD], [Budget MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = DIVIDE(_Target, _AxisMax) * _AxisRange
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _Icon = "<text x='" & _BarMin - 3 & "' y='13.5' font-family='Segoe UI' font-size='6' font-weight='700' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT(_Performance, "^^^;vvv;") & "</text>"
VAR _Label = "<text x='" & _BarMin - 10 & "' y='15' font-family='" & _Font & "' font-size='" & _FontSize & "' font-weight='" & _FontWeight & "' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT(_Performance, "#,##0%;#,##0%;#,##0%") & "</text>"
VAR _TargetBar = "<rect x='" & _BarMin & "' y='10' width='" & _TargetNormalized & "' height='12' stroke='" & _ActualColor & "' fill='" & _TargetColor & "'/>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='3' width='" & _ActualNormalized & "' height='12' stroke='" & _ActualColor & "' fill='" & _ActualColor & "'/>"
VAR _VarianceBar = "<rect x='" & _BarMin + MIN(_ActualNormalized, _TargetNormalized) + 1 & "' y='" & IF(_Target > _Actual, 2.9, 9) & "' width='" & ABS(_ActualNormalized - _TargetNormalized) - 1 & "' height='6' stroke='" & _VarianceColor & "' fill='" & _VarianceColor & "'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _Icon & _Label & _TargetBar & _ActualBar & _VarianceBar & _SvgSuffix
-- SVG Overlapping Bars with Variance
-- Author: Kurt Buhler / Data Goblins (PowerBI MacGuyver Toolbox)
-- Target: Table / Matrix (Image size: H25 x W100)
-- Placeholders: Replace __ACTUAL_MEASURE, __TARGET_MEASURE, __GROUPBY_COLUMN with your fields
-- Input field config
VAR _Actual = __ACTUAL_MEASURE
VAR _Target = __TARGET_MEASURE
VAR _Performance = DIVIDE ( _Actual - _Target, _Target )
-- Font config
VAR _Font = "Segoe UI"
VAR _FontSize = 10
VAR _FontWeight = 600
-- Chart Config
VAR _BarMax = 100
VAR _BarMin = 30
VAR _Scope = ALLSELECTED ( __GROUPBY_COLUMN )
-- Color config
VAR _ActualColor = "#686868"
VAR _TargetColor = "#e1dfdd"
VAR _VarianceColor =
IF (
_Performance < 0,
"#fab005",
"#2094ff"
)
-- Axis normalization
VAR _MaxActualsInScope =
CALCULATE(
MAXX( _Scope, __ACTUAL_MEASURE ),
REMOVEFILTERS( __GROUPBY_COLUMN )
)
VAR _MaxTargetInScope =
CALCULATE(
MAXX( _Scope, __TARGET_MEASURE ),
REMOVEFILTERS( __GROUPBY_COLUMN )
)
VAR _AxisMax =
IF (
HASONEVALUE ( __GROUPBY_COLUMN ),
MAX( _MaxActualsInScope, _MaxTargetInScope ),
CALCULATE( MAX( __ACTUAL_MEASURE, __TARGET_MEASURE ), REMOVEFILTERS( __GROUPBY_COLUMN ) )
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE ( _Actual, _AxisMax ) * _AxisRange
VAR _TargetNormalized = DIVIDE ( _Target, _AxisMax ) * _AxisRange
-- SVG elements
VAR _SvgPrefix = "data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT ( _Actual, "000000000000" ) & "</desc>"
VAR _Icon = "<text x='" & _BarMin - 3 & "' y='13.5' font-family='Segoe UI' font-size='6' font-weight='700' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT ( _Performance, "▲;▼;" ) & "</text>"
VAR _Label = "<text x='" & _BarMin - 10 & "' y='15' font-family='" & _Font & "' font-size='" & _FontSize & "' font-weight='" & _FontWeight & "' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT ( _Performance, "#,##0%;#,##0%;#,##0%" ) & "</text>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='3' width='" & _ActualNormalized & "' height='12' fill='" & _ActualColor & "'/>"
VAR _TargetBar = "<rect x='" & _BarMin & "' y='10' width='" & _TargetNormalized & "' height='12' fill='" & _TargetColor & "'/>"
VAR _VarianceBar = "<rect x='" & _BarMin + MIN( _ActualNormalized, _TargetNormalized ) + 1 & "' y='" & IF ( _Target > _Actual, 2.9, 9 ) & "' width='" & ABS( _ActualNormalized - _TargetNormalized ) - 1 & "' height='6' fill='" & _VarianceColor & "'/>"
VAR _SvgSuffix = "</svg>"
VAR _SVG =
_SvgPrefix
& _Sort
& _Icon & _Label
& _TargetBar & _ActualBar & _VarianceBar
& _SvgSuffix
RETURN _SVG
-- Progress Bar SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Generates a colored progress bar with percentage label
-- Target visual: Table, Matrix, or Card (imageHeight: 20, imageWidth: 100)
Progress Bar SVG =
VAR _Pct = [Completion Percentage] -- 0 to 1 value
VAR _W = 100
VAR _H = 20
VAR _FillW = _Pct * _W
VAR _Label = FORMAT(_Pct, "0%")
VAR _Color =
SWITCH(
TRUE(),
_Pct < 0.5, "#F44336",
_Pct < 0.8, "#FF9800",
"#4CAF50"
)
VAR _Prefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 " & _W & " " & _H & "'>"
VAR _Background = "<rect width='" & _W & "' height='" & _H & "' fill='#E0E0E0' rx='" & (_H / 2) & "'/>"
VAR _Fill = "<rect width='" & _FillW & "' height='" & _H & "' fill='" & _Color & "' rx='" & (_H / 2) & "'/>"
VAR _Text = "<text x='" & (_W / 2) & "' y='" & (_H / 2 + 5) & "' font-size='11' text-anchor='middle' fill='white' font-weight='bold'>" & _Label & "</text>"
VAR _Suffix = "</svg>"
RETURN
_Prefix & _Background & _Fill & _Text & _Suffix
-- Sparkline SVG Measure
-- dataType: Text, dataCategory: ImageUrl
-- Generates a line sparkline for the last 12 months
-- Target visual: Table or Matrix (imageHeight: 30, imageWidth: 100)
Sparkline SVG =
VAR _Values =
ADDCOLUMNS(
CALCULATETABLE(
VALUES('Date'[Month]),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)
),
"@Value", [Sales Amount]
)
VAR _XMin = MIN('Date'[Month])
VAR _XMax = MAX('Date'[Month])
VAR _YMin = MINX(_Values, [@Value])
VAR _YMax = MAXX(_Values, [@Value])
VAR _Points =
CONCATENATEX(
ADDCOLUMNS(
_Values,
"@X", INT(100 * DIVIDE('Date'[Month] - _XMin, _XMax - _XMin)),
"@Y", INT(30 * DIVIDE([@Value] - _YMin, _YMax - _YMin))
),
[@X] & "," & (30 - [@Y]),
" ",
'Date'[Month]
)
VAR _Prefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 30'>"
VAR _Line = "<polyline fill='none' stroke='#5B8DBE' stroke-width='2' points='" & _Points & "'/>"
VAR _Suffix = "</svg>"
RETURN
IF(
HASONEVALUE('Product'[Category]),
_Prefix & _Line & _Suffix,
BLANK()
)
-- SVG Status Pill
-- Author: Kurt Buhler / Data Goblins (PowerBI MacGuyver Toolbox)
-- Target: Table / Matrix (Image size: H25 x W100)
-- Placeholders: Replace __GROUPBY_COLUMN with your column
-- Note: Update the SWITCH values and colors for your specific categories
IF (
HASONEVALUE( __GROUPBY_COLUMN ),
VAR _CategoryValue = SELECTEDVALUE ( __GROUPBY_COLUMN )
VAR _LabelCased = UPPER ( _CategoryValue )
-- Color config -- change "Value" to your actual category values
VAR _Color =
SWITCH (
_CategoryValue,
"Value1", "#1971c2", -- Blue
"Value2", "#2f9e44", -- Green
"Value3", "#e03131", -- Red
"Value4", "#f08c00", -- Yellow
"#000000" -- Default black
)
-- Font config -- change "Value" to bold/lighten specific categories
VAR _FontWeight =
SWITCH (
_CategoryValue,
"Value1", "700", -- Bold
"400" -- Normal
)
-- SVG elements
VAR _SvgPrefix = "data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Background =
"<rect x='0.5' y='0.5' width='98%' height='95%' rx='15%' fill='"
& _Color & "22"
& "' stroke='" & _Color & "'/>"
VAR _Label =
"<text x='50%' y='53%' font-family='Segoe UI' font-size='10.5' font-weight='"
& _FontWeight
& "' fill='" & _Color
& "' text-anchor='middle' dominant-baseline='middle'>"
& _LabelCased & "</text>"
VAR _SvgSuffix = "</svg>"
VAR _SVG = _SvgPrefix & _Background & _Label & _SvgSuffix
RETURN _SVG
)
-- SVG Waterfall Chart
-- Author: Kurt Buhler / Data Goblins (PowerBI MacGuyver Toolbox)
-- Target: Table / Matrix (Image size: H25 x W100)
-- Placeholders: Replace __ACTUAL_MEASURE, __GROUPBY_COLUMN with your fields
-- Note: Uses OFFSET/GENERATEALL for cumulative positioning -- requires Power BI 2023+
-- Input field config
VAR _Actual = __ACTUAL_MEASURE
VAR _LabelFont = "Segoe UI"
VAR _LabelWeight = "600"
VAR _LabelSize = "11"
-- Chart Config
VAR _BarMax = 100
VAR _BarMin = 0
VAR _Scope = ALLSELECTED ( __GROUPBY_COLUMN )
-- Color config
VAR _BarFillColor =
IF ( HASONEVALUE ( __GROUPBY_COLUMN ), "#dad9d8", "#878582" )
VAR _LabelColor =
IF ( HASONEVALUE ( __GROUPBY_COLUMN ), "#87858299", "#EBEBEB" )
-- Number formatting
VAR _NumberFormat =
SWITCH (
TRUE (),
_Actual <= 1E3, FORMAT ( _Actual, "#,0" ),
_Actual <= 1E6, FORMAT ( _Actual, "#,0, K" ),
_Actual <= 1E9, FORMAT ( _Actual, "#,0,, M" ),
_Actual <= 1E12, FORMAT ( _Actual, "#,0,,, bn" )
)
-- Cumulative positioning via OFFSET
VAR _ByCategory =
ADDCOLUMNS ( _Scope, "@Actual", __ACTUAL_MEASURE )
VAR _FilterTable =
SELECTCOLUMNS ( _Scope, "@CategoryPR", __GROUPBY_COLUMN, "@ActualPR", __ACTUAL_MEASURE )
VAR _ResultTable =
GENERATEALL ( _ByCategory, OFFSET ( -1, _FilterTable, ORDERBY ( [@ActualPR] ) ) )
VAR _CumulationPR = FILTER ( _ResultTable, [@ActualPR] >= _Actual )
VAR _CumulativeAmountPR = SUMX ( _CumulationPR, [@Actual] )
VAR _Total = SUMX ( _ByCategory, [@Actual] )
VAR _CumulativePercentagePR = DIVIDE ( _CumulativeAmountPR, _Total )
VAR _PercentageByCategoryPR = IF ( HASONEVALUE ( __GROUPBY_COLUMN ), _CumulativePercentagePR )
-- Normalize
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE ( _Actual, _Total ) * _AxisRange
VAR _StartPos = _PercentageByCategoryPR * _AxisRange
VAR _EndPos = MIN ( _StartPos + _ActualNormalized, 99 )
-- SVG elements
VAR _SvgPrefix = "data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT ( _Actual, "000000000000" ) & "</desc>"
VAR _ActualBar = "<rect x='" & MAX ( _StartPos, 0 ) & "' y='3' width='" & _ActualNormalized & "' height='75%' fill='" & _BarFillColor & "'/>"
VAR _ConnectorStart = "<rect x='" & _StartPos & "' y='0' width='0.5' height='100%' fill='#333333'/>"
VAR _ConnectorEnd = "<rect x='" & _EndPos & "' y='0' width='0.5' height='100%' fill='#333333'/>"
VAR _ActualLabelRS = "<text x='" & _EndPos + 4 & "' y='16' font-family='" & _LabelFont & "' font-size='" & _LabelSize & "' font-weight='" & _LabelWeight & "' text-anchor='start' fill='" & _LabelColor & "'>" & _NumberFormat & "</text>"
VAR _ActualLabelLS = "<text x='" & _StartPos - 4 & "' y='16' font-family='" & _LabelFont & "' font-size='" & _LabelSize & "' font-weight='" & _LabelWeight & "' text-anchor='end' fill='" & _LabelColor & "'>" & _NumberFormat & "</text>"
VAR _ActualLabel = IF ( _EndPos > _AxisRange * 0.5, _ActualLabelLS, _ActualLabelRS )
VAR _SvgSuffix = "</svg>"
VAR _SVG =
_SvgPrefix & _Sort
& _ActualBar & _ConnectorStart & _ConnectorEnd & _ActualLabel
& _SvgSuffix
RETURN _SVG
SVG Community Examples and Libraries
Organized by target visual type. Use these as reference when building SVG DAX measures.
Libraries (UDF-based)
| Library | Author | Visual Target | Key Features | URL |
|---|---|---|---|---|
| PowerofBI.IBCS | Andrzej Leszkiewicz | Table/Matrix | IBCS-compliant bar, column, waterfall, pin, small multiples, P&L | https://daxlib.org/package/PowerofBI.IBCS/ |
| DaxLib.SVG | Jake Duddy | Table/Matrix | UDF library with 3-tier API (Viz/Compound/Element) | https://daxlib.org/package/DaxLib.SVG/ (source: https://github.com/daxlib/dev-daxlib-svg) |
| PBI-Core-Visuals-SVG-HTML | Various contributors | Table/Matrix | Chips, tornado, gradient matrix, bar UDF | https://github.com/nickvdw2/PBI-Core-Visuals-SVG-HTML |
| PowerBI MacGuyver Toolbox | Stepan Resl / Data Goblins | Card/Image | 20+ bar, 14+ line, 24+ KPI templates | https://github.com/data-goblin/powerbi-macguyver-toolbox |
| Dashboard Design UDF Library | Dashboard-Design | Table/Matrix | Target line bars, pill visuals | https://github.com/Dashboard-Design/Dashboard-Design-UDF-Library |
| Kerry Kolosko Templates | Kerry Kolosko | Image/Table/Matrix | Sparklines, data bars, gauges, KPI cards | https://kerrykolosko.com/portfolio-category/svg-templates/ |
See also references/svg-table-matrix.md for the UDF pattern and calling convention.
PowerofBI.IBCS Functions (Andrzej Leszkiewicz)
IBCS-compliant SVG chart functions for business reporting. Install via DaxLib: https://daxlib.org/package/PowerofBI.IBCS/ -- source: https://github.com/avatorl/dax-udf-svg-ibcs
All functions target Table/Matrix visuals. Uses DAX UDF syntax with typed parameters.
Bar Charts (horizontal)
| Function | Description |
|---|---|
PowerofBI.IBCS.BarChart.AbsoluteValues | AC bar vs PY/BU base bar with data label. Base styling: "grey" (PY) or "outlined" (BU) |
PowerofBI.IBCS.BarChart.AbsoluteVariance | Diverging bar showing absolute delta (AC-PY). Green/red by businessImpact. Hatched fill when FC present |
PowerofBI.IBCS.BarChart.RelativeVariance | Pin chart showing % delta. Capped at +/-100% with outlier triangles. Hatched pinhead when FC present |
PowerofBI.IBCS.BarChart.WithAbsoluteVariance | Compact: PY grey bar + AC black bar + colored variance bar. Auto-scaled to ALLSELECTED |
Column Charts (vertical)
| Function | Description |
|---|---|
PowerofBI.IBCS.ColumnChart.WithAbsoluteVariance | Monthly AC/FC columns + PY triangles + variance bars. Hatched FC, solid AC |
PowerofBI.IBCS.ColumnChart.WithWaterfall | Multi-tier: columns + waterfall + relative pins. 3 tiers or responsive CSS. Most complex function |
PowerofBI.IBCS.ColumnChart.Stacked | Stacked columns with top-N product groups + "Other". Highlight selected group |
PowerofBI.IBCS.ColumnChart.SmallMultiple | Small multiple cards: absolute values, absolute variance, or relative variance modes |
PowerofBI.IBCS.ColumnChart.AbsoluteVarianceWide | Wide single-column: AC vs PY with variance bar and labels |
Waterfall / P&L
| Function | Description |
|---|---|
PowerofBI.IBCS.Waterfall.Vertical | Vertical P&L waterfall with hierarchical structure (level-1/level-2). AC/PY/PL styling. Connector lines, subtotal markers |
Extras
| Function | Description |
|---|---|
PowerofBI.IBCS.Extras.PieChart.PctOfTotal | Pie chart showing percentage of total. Optional toggle, bold totals |
PowerofBI.IBCS.Helpers.Title | Multi-line IBCS title (what/how/when) joined with UNICHAR(10) |
DaxLib.SVG Functions (Jake Duddy)
High-level Viz.* functions -- each produces a complete SVG data URI for Table/Matrix Image URL columns:
| Function | Chart Type |
|---|---|
Viz.Area | Area chart |
Viz.Bars | Bar chart |
Viz.Boxplot | Box plot |
Viz.Heatmap | Heatmap |
Viz.Jitter | Jitter plot |
Viz.Line | Line / Sparkline |
Viz.Pill | Pill badge |
Viz.ProgressBar | Progress bar |
Viz.Violin | Violin plot |
Mid-level Compound.* functions create positionable chart components you combine with SVG() wrapper. Low-level Element.* functions create raw SVG primitives (rect, circle, line, polyline, text, path, group).
Docs: https://evaluationcontext.github.io/daxlib.svg/
Examples by Target Visual: Table / Matrix
SVG measures rendered in table/matrix cells via Image URL column binding. DaxLib.SVG and PowerofBI.IBCS functions (listed above) all target Table/Matrix.
Kerry Kolosko (kerrykolosko.com/portfolio/)
| Template | Chart Type | URL |
|---|---|---|
| Progress Callout | Progress bar with % | https://kerrykolosko.com/portfolio/progress-callout/ |
| Progress Bars | Bullet + progress | https://kerrykolosko.com/portfolio/progress-bars/ |
| Gradient Area Sparkline | Area sparkline with last point | https://kerrykolosko.com/portfolio/gradient-area-sparkline-with-last-point/ |
| Sparklines | Area + gradient sparkline | https://kerrykolosko.com/portfolio/sparklines/ |
| Gradient Sparklines | Line + area gradient | https://kerrykolosko.com/portfolio/gradient-sparklines/ |
| Circular Gauges | Pie gauge + radial gauge | https://kerrykolosko.com/portfolio/circular-gauges/ |
| Range Bars | Min/max/avg range | https://kerrykolosko.com/portfolio/range-bars/ |
| KPI Card | KPI with gauge bar | https://kerrykolosko.com/portfolio/kpi-card/ |
| Data Bars | Diverging pos/neg bars | https://kerrykolosko.com/portfolio/data-bars/ |
PowerBI MacGuyver Toolbox (Kurt Buhler / Data Goblins, SVG measures by Stepan Resl)
MacGuyver Toolbox provides both native Power BI visual templates (bar/line/KPI patterns) and SVG DAX measures. The SVG measures are added via C# scripts in Tabular Editor. See examples/ for extracted DAX measures from this toolbox.
Examples by Target Visual: Image
SVG measures rendered in standalone Image visuals via sourceType='imageData'.
Kerry Kolosko (kerrykolosko.com/portfolio/)
| Template | Chart Type | URL |
|---|---|---|
| Gauges with Tracks | Semicircular gauge | https://kerrykolosko.com/portfolio/gauges-with-tracks/ |
| Gauge with States | Radial gauge with dial | https://kerrykolosko.com/portfolio/gauge-with-states/ |
| Sparklines with Intercept | Line sparkline + intercept line | https://kerrykolosko.com/portfolio/sparklines-with-intercept/ |
| Area Sparklines with Intercept | Area sparkline + intercept | https://kerrykolosko.com/portfolio/area-sparklines-with-intercept/ |
| Barcode & Jitter Scatter | Barcode + jitter plot | https://kerrykolosko.com/portfolio/barcode-jitter-scatter/ |
| Waterfall | Waterfall chart | https://kerrykolosko.com/portfolio/waterfall/ |
| Boxplots and Dumbells | Box plot + dumbbell | https://kerrykolosko.com/portfolio/boxplots-and-dumbells/ |
| Radial Plot Backgrounds | Concentric axis backgrounds | https://kerrykolosko.com/portfolio/radial-plot-backgrounds/ |
| Progress with Icons | Progress bar + icon callouts | https://kerrykolosko.com/portfolio/progress-with-icons/ |
| Lollipop Sparkline | Lollipop sparkline + square variant | https://kerrykolosko.com/portfolio/lollipop-sparkline/ |
Examples by Target Visual: Card (New)
SVG measures rendered in the new Card visual via callout.imageFX.
PowerBI MacGuyver Toolbox (Stepan Resl / Data Goblins)
KPI card templates using SVG measures for inline micro-charts in card visuals:
| Template | Chart Type | Repo Path |
|---|---|---|
| KPI Bar | Bar in card | kpi-cards/kpi-bar/ |
| KPI Bullet | Bullet in card | kpi-cards/kpi-bullet/ |
| KPI Doughnut | Donut in card | kpi-cards/kpi-doughnut/ |
| KPI Gauge | Gauge in card | kpi-cards/kpi-gauge/ |
| KPI Sparkline Trend | Sparkline in card | kpi-cards/kpi-sparkline-trend/ |
| KPI Trend Bar | Trend bar in card | kpi-cards/kpi-trend-bar/ |
| KPI Trend Comparison | Trend comparison in card | kpi-cards/kpi-trend-comparison/ |
| KPI Trend Line | Trend line in card | kpi-cards/kpi-trend-line/ |
| Waffle Text | Waffle in card | kpi-cards/waffle-text/ |
| Star Rating Text | Star rating in card | kpi-cards/star-rating-text/ |
Repo: https://github.com/data-goblin/powerbi-macguyver-toolbox
SVG Measures: Accessibility
A DAX SVG measure renders as an image keyed off ImageUrl; the screen-reader path reads the visual's title, type, author alt text, and the "Show data" table. A cell whose value is an svg+xml URI contributes nothing meaningful to either path (Show-data lists the literal markup string), and there is no per-cell alt-text slot. Every inline SVG chart is an accessibility dead zone by default.
Mitigation at the host-visual level
Apply both of the following for any SVG measure encoding primary KPI data; the second alone is sufficient for decorative micro-charts.
1. Keep the numbers in adjacent readable columns
For an SVG sparkline column, also bind the numeric measures the SVG encodes (min, max, last value; actual, target, variance) as plain value columns in the same table or matrix. Show-data then carries real numbers rather than markup strings, satisfying WCAG 1.1.1 for the data-table fallback.
Decide per project which columns to expose vs hide at the visual level (they can be hidden from the visual but still present in the query so Show-data picks them up).
2. Set dynamic alt text on the host container
Author a sibling _Alt measure returning a spoken sentence (under ~250 characters) that narrates the SVG's content for the current filter context:
KPI Sparkline Alt =
VAR _Last = [Sales Amount]
VAR _Trend = IF([Sales Amount] > [Sales Amount PY], "up", "down")
RETURN
"Sales: " & FORMAT(_Last, "$#,0,, M") & ", trending " & _Trend & " vs prior year"Bind via pbir set:
pbir set "MyPage/MyTable.Visual" "general.altText" --measure "_Report.KPI Sparkline Alt"Or set directly in visual.json under visualContainerObjects.general[0].properties.altText:
"general": [{
"properties": {
"altText": {
"expr": {
"Measure": {
"Expression": { "SourceRef": { "Schema": "extension", "Entity": "Sales" } },
"Property": "KPI Sparkline Alt"
}
}
}
}
}]The measure must guard BLANK() (a blank alt text is no better than none):
IF(COUNTROWS(VALUES('Date'[Month])) > 0,
"Sales: " & FORMAT(_Last, "$#,0,, M") & "...",
"No data for the current selection."
)For an image visual rendering a single SVG, the container has a general.altText slot; always set it. The static literal form is acceptable when the SVG is not filter-sensitive.
Decorative SVGs
For purely decorative SVGs (dividers, brand backgrounds), mark the host hidden in tab order so screen readers skip it entirely:
pbir set "MyPage/MyDecorativeSVG.Visual" "position.tabOrder" -1Color-only encoding
An SVG that conveys status purely via fill/stroke color fails WCAG 1.4.1. Pair every semantic color with at least one of:
- A shape or glyph change (circle vs diamond, filled vs hollow)
- A text label or abbreviation inside the SVG
- A
<title>element inside the SVG markup itself
The status pill pattern already does this (text label alongside fill color). The dumbbell and bullet patterns do not; add a symbol or label if they are primary status indicators.
Contrast
SVG elements are subject to the same WCAG contrast requirements as rasterized content:
- Text inside the SVG: >= 4.5:1 against the cell background (3:1 for large text >= 18pt)
- Graphical elements (bars, lines, dots used for data): >= 3:1 against adjacent colors
Theme tokens from the host report's theme do not automatically propagate into an SVG string; verify contrast manually, especially on dark themes or high-contrast accessibility themes.
What the Desktop Bridge screenshot does not confirm
The Bridge screenshot is useful for layout verification; it does not confirm:
- Alt text is populated and readable
- Adjacent readable columns are present in Show-data
- Tab order positions the visual correctly for keyboard users
Confirm accessibility by reading back the bound measure expression and auditing visual.json for general.altText and the presence of non-SVG bound columns.
Severity guidance for audit findings
When surfaced by the review-report skill:
- Primary KPI SVGs with no alt text and no adjacent numeric column: high severity
- SVG measures with color-only encoding and no paired shape or label: high severity
- Decorative micro-charts with no alt text: medium severity (low if clearly decorative and tab-order is -1)
SVG Patterns for Card and Slicer Visuals
Card (cardVisual) and Slicer (advancedSlicerVisual) visuals support SVG measures through specific binding patterns. The classic card does NOT support SVG -- only the new card visual works.
Card Visual (cardVisual)
Binding
Card visuals render SVG via callout.imageFX. Bind the SVG measure to the card's calloutValue field, then configure imageFX:
{
"objects": {
"callout": [{
"properties": {
"imageFX": {"expr": {"Literal": {"Value": "true"}}},
"imageHeight": {"expr": {"Literal": {"Value": "40D"}}},
"imageWidth": {"expr": {"Literal": {"Value": "100D"}}}
}
}]
}
}Pattern: Arrow Indicator
Compact directional indicator for KPI cards.
Arrow Indicator =
VAR _Growth = [Growth %]
VAR _Up = _Growth >= 0
VAR _Path = IF(_Up, "M 10,15 L 5,10 L 15,10 Z", "M 10,5 L 5,10 L 15,10 Z")
VAR _Color = IF(_Up, "#4CAF50", "#F44336")
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>" &
"<path d='" & _Path & "' fill='" & _Color & "'/></svg>"Pattern: Mini Gauge
Semi-circular gauge for progress or performance.
Mini Gauge =
VAR _Pct = DIVIDE([Value], [Target], 0)
VAR _Angle = (_Pct * 180) - 90
VAR _R = 40
VAR _CX = 50
VAR _CY = 50
VAR _Rad = _Angle * PI() / 180
VAR _NX = _CX + (_R * 0.8 * COS(_Rad))
VAR _NY = _CY + (_R * 0.8 * SIN(_Rad))
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 60'>" &
"<path d='M 10 50 A 40 40 0 0 1 90 50' fill='none' stroke='#E0E0E0' stroke-width='8'/>" &
"<line x1='" & _CX & "' y1='" & _CY & "' x2='" & _NX & "' y2='" & _NY & "' stroke='#333' stroke-width='2'/>" &
"<circle cx='" & _CX & "' cy='" & _CY & "' r='3' fill='#333'/></svg>"Pattern: Mini Donut
Percentage completion as a donut ring.
Mini Donut =
VAR _Pct = [Percentage]
VAR _Angle = _Pct * 360
VAR _LargeArc = IF(_Angle > 180, 1, 0)
VAR _R = 40
VAR _CX = 50
VAR _CY = 50
VAR _Rad = (_Angle - 90) * PI() / 180
VAR _EndX = _CX + (_R * COS(_Rad))
VAR _EndY = _CY + (_R * SIN(_Rad))
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>" &
"<circle cx='" & _CX & "' cy='" & _CY & "' r='" & _R & "' fill='none' stroke='#E0E0E0' stroke-width='8'/>" &
"<path d='M " & _CX & " " & (_CY - _R) & " A " & _R & " " & _R & " 0 " & _LargeArc & " 1 " & _EndX & " " & _EndY & "' fill='none' stroke='#2196F3' stroke-width='8'/></svg>"Pattern: Progress Bar
Horizontal bar with label, sized for card visuals.
Progress Bar =
VAR _Pct = [Completion %]
VAR _W = 100
VAR _H = 20
VAR _FillW = _Pct * _W
VAR _Label = FORMAT(_Pct, "0%")
VAR _Color = SWITCH(TRUE(), _Pct < 0.5, "#F44336", _Pct < 0.8, "#FF9800", "#4CAF50")
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 " & _W & " " & _H & "'>" &
"<rect width='" & _W & "' height='" & _H & "' fill='#E0E0E0' rx='" & (_H / 2) & "'/>" &
"<rect width='" & _FillW & "' height='" & _H & "' fill='" & _Color & "' rx='" & (_H / 2) & "'/>" &
"<text x='" & (_W / 2) & "' y='" & (_H / 2 + 5) & "' font-size='11' text-anchor='middle' fill='white' font-weight='bold'>" & _Label & "</text></svg>"Pattern: Narrative Sentence
When the story needs inline formatting that reacts to data (a clause that changes color on a miss, a bold figure, a verdict word keyed off performance), compose the whole sentence as a DAX measure returning an SVG with <text> and <tspan> runs. This is the only path to "X out of Y targets hit (~Z%)" where the number, color, and verdict word all key off data in a single declarative, version-controlled measure.
Decision guide for narrative elements:
- Plain "label: value" -> dynamic-value textbox run (simpler, inherits measure format string)
- Whole-string title with no per-clause styling -> expression-based DAX visual title
- Conditionally-styled clauses or sentence with inline micro-chart -> SVG narrative measure (below)
- Multi-paragraph AI summary -> Narrative visual (non-deterministic, license-gated, cannot be diffed; avoid for deterministic reporting)
Performance Narrative =
VAR _Actual = [Sales Amount]
VAR _Target = [Sales Target]
VAR _Variance = DIVIDE(_Actual - _Target, _Target)
VAR _Hit = _Actual >= _Target
VAR _VerdictText = IF(_Hit, "on track", "behind")
VAR _VerdictColor = IF(_Hit, "#2D6A2E", "#982F2F")
VAR _ActualFmt = FORMAT(_Actual, "$#,0,, M")
VAR _TargetFmt = FORMAT(_Target, "$#,0,, M")
VAR _VarFmt = FORMAT(ABS(_Variance), "+0.0%;0.0%")
RETURN
"data:image/svg+xml;utf8," &
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 30'>" &
-- Plain prefix
"<text x='4' y='20' font-family='Segoe UI' font-size='12' fill='#333'>" &
"Sales " &
-- Bold actual value, brand color
"<tspan font-weight='700' fill='#1f4e79'>" & _ActualFmt & "</tspan>" &
" vs target " &
"<tspan fill='#555'>" & _TargetFmt & "</tspan>" &
" -- " &
-- Verdict: color changes with performance
"<tspan fill='" & _VerdictColor & "' font-weight='700'>" & _VerdictText & " (" & _VarFmt & ")</tspan>" &
"</text>" &
"</svg>"Key notes:
- Each styled clause is a separate
<tspan>carrying its ownfillandfont-weight; mix styled and unstyled runs in one<text>element - Colors come from
IF/SWITCHover data; use hex codes aligned with the report's theme tokens - Format numbers inside the measure via
FORMAT(); the SVG string ignores the measure's model format string - The 32K limit and no-interactivity constraints apply; keep sentences concise
- The classic card (
card) does not supportImageUrlcallouts; usecardVisual, animagevisual, or a table single-row image column
For the binding, wire the measure to callout.imageFX in a cardVisual (see the binding block above), or to sourceField in an image visual. To embed a tiny inline sparkline mid-sentence, add a <polyline> element after the text run within the same SVG.
---
Slicer Visual (advancedSlicerVisual)
Slicers can display SVG in header images and custom slicer items. This is less common but useful for branded slicer headers.
Binding
Set SVG in slicer header via header.image:
{
"objects": {
"header": [{
"properties": {
"image": {
"expr": {
"Measure": {
"Expression": {"SourceRef": {"Schema": "extension", "Entity": "Table"}},
"Property": "Header SVG"
}
}
}
}
}]
}
}---
Design Considerations
Card SVGs Should Be Small
Card visuals have limited space. Keep SVGs compact:
- Arrow indicators:
viewBox='0 0 20 20' - Mini gauges:
viewBox='0 0 100 60' - Progress bars:
viewBox='0 0 100 20'
Classic Card Does NOT Work
The classic card visual (card) does NOT support SVG measures. Only cardVisual (the "new card") works. If SVG renders as text or blank, verify the visual type is cardVisual.
Color Rules
Same as all SVG visuals: always use hex codes with # (e.g., fill='#2196F3'). Never use %23 URL encoding.
Conditional Colors for KPI States
Common color scheme for KPI indicators:
VAR _Color = SWITCH(TRUE(),
_Performance >= 1.0, "#4CAF50", -- Green (exceeds target)
_Performance >= 0.8, "#FF9800", -- Amber (near target)
"#F44336" -- Red (below target)
)Or using the SpaceParts sentiment pattern with 4 levels:
VAR _Color = SWITCH(TRUE(),
_Performance < -0.05, "#f4ae4c", -- Dark yellow (very bad)
_Performance < -0.025, "#ffe075", -- Light yellow (bad)
_Performance > 0.05, "#2D6390", -- Dark blue (very good)
_Performance > 0.025, "#74B2FF", -- Light blue (good)
"#CCCCCC" -- Grey (neutral)
)SVG Elements Reference for DAX
Quick reference for SVG elements commonly used in DAX measures. All examples use single quotes for SVG attributes to avoid DAX double-quote escaping.
Rectangle
"<rect x='10' y='5' width='50' height='10' fill='#2196F3' rx='2'/>"| Attribute | Description |
|---|---|
| x, y | Position |
| width, height | Dimensions |
| fill | Fill color |
| stroke | Border color |
| stroke-width | Border thickness |
| opacity | 0-1 transparency |
| rx, ry | Corner radius |
Circle
"<circle cx='50' cy='10' r='5' fill='#F44336'/>"| Attribute | Description |
|---|---|
| cx, cy | Center position |
| r | Radius |
| fill, stroke, opacity | Styling |
Line
"<line x1='0' y1='10' x2='100' y2='10' stroke='#333333' stroke-width='2'/>"| Attribute | Description |
|---|---|
| x1, y1 | Start point |
| x2, y2 | End point |
| stroke | Color |
| stroke-width | Thickness |
| stroke-dasharray | Dash pattern (e.g., '4,2') |
Polyline (Sparklines)
"<polyline fill='none' stroke='#01B8AA' stroke-width='3' points='0,50 10,30 20,40 30,10'/>"| Attribute | Description |
|---|---|
| points | Space-separated x,y pairs |
| fill | none for line only, color for area fill |
| stroke | Line color |
| stroke-width | Line thickness |
Build points with CONCATENATEX:
VAR Lines = CONCATENATEX(Table, [X] & "," & [Y], " ", [SortColumn])Text
"<text x='50' y='10' font-size='12' fill='#333333' font-weight='bold' text-anchor='middle' dominant-baseline='middle'>Label</text>"| Attribute | Description |
|---|---|
| x, y | Position |
| font-size | Size in px |
| fill | Text color |
| font-weight | normal, bold, 700 |
| font-family | Segoe UI recommended |
| text-anchor | start, middle, end |
| dominant-baseline | auto, middle, hanging |
Path (Arcs, Curves)
"<path d='M 10,10 L 50,10 L 30,30 Z' fill='#4CAF50'/>"| Command | Meaning | Example |
|---|---|---|
| M x,y | Move to | M 10,10 |
| L x,y | Line to | L 50,10 |
| A rx ry rot large-arc sweep x y | Arc | A 40 40 0 0 1 90 50 |
| C x1 y1 x2 y2 x y | Cubic bezier | |
| Q x1 y1 x y | Quadratic bezier | |
| Z | Close path |
Arc for gauge/donut:
"<path d='M 10 50 A 40 40 0 0 1 90 50' fill='none' stroke='#2196F3' stroke-width='8'/>"Group
"<g transform='translate(10,10)'>" & _Shape1 & _Shape2 & "</g>"| Transform | Example |
|---|---|
| translate(x, y) | Move group |
| rotate(angle) | Rotate around origin |
| scale(x, y) | Scale group |
Gradient Definition
"<defs><linearGradient id='grad' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='#0000FF'/><stop offset='1' stop-color='#00000000'/></linearGradient></defs>"Reference in fill: fill='url(#grad)'
| Attribute | Description |
|---|---|
| id | Reference name |
| x1, y1, x2, y2 | Gradient direction (0-1 normalized) |
| gradientUnits | objectBoundingBox (default) or userSpaceOnUse |
| stop offset | Position along gradient (0-1) |
| stop-color | Color at this stop |
SVG Container
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>"| Attribute | Description |
|---|---|
| xmlns | Required: http://www.w3.org/2000/svg |
| viewBox | Coordinate system: minX minY width height |
| width, height | Fixed dimensions (optional with viewBox) |
| preserveAspectRatio | none to stretch, xMidYMid meet to maintain ratio |
Common Patterns
Responsive sizing
Use viewBox instead of fixed width/height:
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 30'>"Hex colors
Always use # directly in SVG attributes -- e.g., fill='#01B8AA'. Do not use %23 URL encoding -- this causes VisualDataProxyExecutionUnknownError in image visuals and is unreliable in other visual types. Avoid named colors (blue, red) -- always use hex.
VAR LineColor = "#01B8AA"Coordinate inversion
SVG Y=0 is at top; invert for charts:
VAR _Y = 100 - [NormalizedValue] -- Flips so higher values go upRender order
Elements render in document order (first = back, last = front). Place backgrounds before foreground elements.
SVG Patterns for Image Visuals
Image visuals (image visual type) render SVG measures as standalone graphics on the report canvas. Unlike table/matrix SVGs (which are inline micro-charts in rows), image visuals occupy their own visual container and can be any size.
Critical: sourceType Must Be 'imageData'
For data:image/svg+xml;utf8,... data URIs, the image visual must use sourceType = 'imageData'. Using 'imageUrl' (which is for HTTP URLs) causes VisualDataProxyExecutionUnknownError and renders black.
Binding an SVG Measure to an Image Visual
Via JSON
Create the image visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure). Set sourceType to 'imageData' and bind the sourceField to the SVG measure reference (e.g., _Fmt.SparklineSVG).
Via Python API
Create the visual.json file manually (see the pbir-format skill in the pbip plugin for JSON structure).
# Example using pbir_object_model (if available):
report.add_extension_measure(
table="Orders",
name="KPI Header SVG",
expression='''...SVG DAX expression...''',
data_type="Text",
data_category="ImageUrl",
display_folder="SVG Charts",
)
# Create image visual and bind to the measure
page = report.pages[0]
visual = page.add_visual("image", x=100, y=50, width=400, height=200)
visual.set_image_source("measure", measure_ref="Orders.KPI Header SVG")
report.save()JSON Structure
The image visual uses objects.image with these properties:
{
"objects": {
"image": [{
"properties": {
"sourceType": {"expr": {"Literal": {"Value": "'imageData'"}}},
"transparency": {"expr": {"Literal": {"Value": "0D"}}},
"sourceField": {
"expr": {
"Measure": {
"Expression": {
"SourceRef": {"Schema": "extension", "Entity": "Orders"}
},
"Property": "KPI Header SVG"
}
}
},
"effects": {"expr": {"Literal": {"Value": "false"}}}
}
}]
}
}Note: image visuals need no query block -- only objects.image with sourceType, sourceField, and optionally transparency/effects.
---
Pattern: KPI Header Card
A standalone SVG that shows a metric value, label, and trend indicator. Designed for image visuals.
KPI Header SVG =
VAR _Value = [Total Revenue]
VAR _PY = [Total Revenue PY]
VAR _Change = DIVIDE(_Value - _PY, _PY)
VAR _ChangeLabel = FORMAT(_Change, "+#,##0.0%;-#,##0.0%")
VAR _ValueLabel = FORMAT(_Value, "$#,##0,, M")
VAR _ChangeColor = IF(_Change >= 0, "#2D6A2E", "#982F2F")
VAR _Arrow = IF(_Change >= 0,
"<polygon points='170,28 175,20 180,28' fill='" & _ChangeColor & "'/>",
"<polygon points='170,20 175,28 180,20' fill='" & _ChangeColor & "'/>"
)
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 60'>" &
"<text x='10' y='20' font-family='Segoe UI' font-size='11' fill='#666' font-weight='600'>TOTAL REVENUE</text>" &
"<text x='10' y='48' font-family='Segoe UI' font-size='28' fill='#333' font-weight='700'>" & _ValueLabel & "</text>" &
_Arrow &
"<text x='185' y='28' font-family='Segoe UI' font-size='12' fill='" & _ChangeColor & "' font-weight='600'>" & _ChangeLabel & " vs PY</text>" &
"</svg>"Design notes:
- Use a wide
viewBox(e.g., 300x60) since image visuals are typically wider than table cells - Include all text labels inside the SVG -- no separate visual title needed
- Font sizes can be larger than table SVGs (28px value vs 10-12px in tables)
---
Pattern: Sparkline with Endpoint Dot
A clean sparkline with a highlighted endpoint, suitable for image visuals in dashboards.
Sparkline with Endpoint =
VAR _Values = ADDCOLUMNS(
CALCULATETABLE(VALUES('Date'[Month]),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)),
"@Value", [Sales Amount]
)
VAR _XMin = MIN('Date'[Month])
VAR _XMax = MAX('Date'[Month])
VAR _YMin = MINX(_Values, [@Value])
VAR _YMax = MAXX(_Values, [@Value])
VAR _Points = CONCATENATEX(
ADDCOLUMNS(_Values,
"@X", INT(280 * DIVIDE('Date'[Month] - _XMin, _XMax - _XMin)) + 10,
"@Y", INT(50 * DIVIDE([@Value] - _YMin, _YMax - _YMin))
),
[@X] & "," & (55 - [@Y]),
" ",
'Date'[Month]
)
VAR _LastX = INT(280 * 1) + 10
VAR _LastVal = MAXX(FILTER(_Values, 'Date'[Month] = _XMax), [@Value])
VAR _LastY = 55 - INT(50 * DIVIDE(_LastVal - _YMin, _YMax - _YMin))
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 60'>" &
"<polyline fill='none' stroke='#448FD6' stroke-width='2' points='" & _Points & "'/>" &
"<circle cx='" & _LastX & "' cy='" & _LastY & "' r='4' fill='#448FD6'/>" &
"</svg>"---
Pattern: Multi-Metric Dashboard Tile
Combines multiple values and a mini trend line in a single image visual.
Dashboard Tile SVG =
VAR _Revenue = [Total Revenue]
VAR _Target = [Revenue Target]
VAR _Pct = DIVIDE(_Revenue, _Target)
VAR _RevLabel = FORMAT(_Revenue, "$#,0,, M")
VAR _PctLabel = FORMAT(_Pct, "0%")
VAR _BarW = _Pct * 180
-- Colors
VAR _BarColor = SWITCH(TRUE(), _Pct < 0.5, "#F44336", _Pct < 0.8, "#FF9800", "#4CAF50")
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 80'>" &
-- Label
"<text x='10' y='16' font-family='Segoe UI' font-size='10' fill='#999' font-weight='600'>REVENUE vs TARGET</text>" &
-- Value
"<text x='10' y='42' font-family='Segoe UI' font-size='22' fill='#333' font-weight='700'>" & _RevLabel & "</text>" &
-- Progress bar
"<rect x='10' y='52' width='180' height='8' fill='#E8E8E8' rx='4'/>" &
"<rect x='10' y='52' width='" & _BarW & "' height='8' fill='" & _BarColor & "' rx='4'/>" &
-- Percentage label
"<text x='10' y='72' font-family='Segoe UI' font-size='10' fill='" & _BarColor & "' font-weight='600'>" & _PctLabel & " of target</text>" &
"</svg>"---
Design Considerations for Image Visuals
viewBox Sizing
Image visuals are flexible in size. Use a viewBox that matches the visual's aspect ratio:
| Use Case | Recommended viewBox | Visual Size |
|---|---|---|
| KPI card | 0 0 300 60 | 300x60 px |
| Sparkline | 0 0 300 50 | 300x50 px |
| Dashboard tile | 0 0 200 80 | 200x80 px |
| Full-width banner | 0 0 600 40 | 600x40 px |
Colors
Always use hex codes with # (e.g., fill='#2196F3'). Never use %23 URL encoding -- this causes rendering failures in image visuals. Named colors like blue or red are unreliable; always use hex.
Transparency and Effects
Disable default image effects to prevent unwanted styling:
"transparency": {"expr": {"Literal": {"Value": "0D"}}},
"effects": {"expr": {"Literal": {"Value": "false"}}}No Query Block
Image visuals bound to SVG measures do not need a query block. The measure is evaluated directly via sourceField. Adding a query block can cause duplicate evaluations.
---
Dynamic and Responsive SVG
Responsive width (image and card only)
Set the SVG root to width='100%' height='100%' and drive geometry off the viewBox; the host scales the rendered image to fill its container:
VAR _Prefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 300 60' preserveAspectRatio='xMidYMid meet'>"This works in image visuals and card callouts. Table and matrix cells ignore percentage width; they use grid.imageWidth from the visual's objects.grid. Fix the viewBox geometry to the configured imageWidth when targeting table cells.
SVG can scale to fill the host, but it cannot read the container width as a number, so true reflow (re-binning bars to available width) is not possible in a DAX SVG measure. Reach for Deneb when the layout must adapt to container width.
Conditional layout based on context
Branch the entire element assembly on filter context so the measure emits a different shape depending on what is selected, keeping each branch under the 32K string ceiling:
Conditional SVG =
VAR _Mode =
SWITCH(
TRUE(),
ISBLANK([Sales Amount]), "empty",
HASONEVALUE('Date'[Month]), "single",
"trend"
)
-- Assemble the data-URI prefix once, outside the branch
VAR _Prefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 60'>"
VAR _Suffix = "</svg>"
-- Build only the branch that will be returned
VAR _EmptyMarkup = "<text x='10' y='35' font-family='Segoe UI' font-size='11' fill='#999'>n/a</text>"
VAR _BarMarkup = -- single-bar assembly (HASONEVALUE context)
"<rect x='10' y='20' width='" & INT(DIVIDE([Sales Amount], [Sales Target]) * 200) & "' height='20' fill='#448FD6'/>"
VAR _SparkMarkup = -- sparkline assembly (multi-period context)
"<polyline fill='none' stroke='#448FD6' stroke-width='2' points='" &
CONCATENATEX(
ADDCOLUMNS(VALUES('Date'[Month]), "@V", [Sales Amount]),
INT(RANKX(VALUES('Date'[Month]), 'Date'[Month], , ASC) * 25) & "," &
INT(50 - DIVIDE([@V], MAXX(VALUES('Date'[Month]), [Sales Amount])) * 40),
" ", 'Date'[Month]
) & "'/>"
VAR _Body =
SWITCH(
_Mode,
"empty", _EmptyMarkup,
"single", _BarMarkup,
_SparkMarkup
)
RETURN _Prefix & _Body & _SuffixKey rules for conditional layout measures:
- Always include a blank/empty-state branch so filter contexts with no data emit a readable fallback rather than a degenerate zero-width SVG or BLANK
- Assemble the data-URI prefix and
</svg>suffix once outside the SWITCH so the empty branch cannot accidentally omit them - Switch on report/page-level state for the whole measure (e.g.,
HASONEVALUEover a slicer's dimension); switching per row on a different condition confuses readers who see different chart metaphors in adjacent cells - Subtotal and total rows should usually return the empty markup or
BLANK()rather than a coarced single-value chart
SVG Patterns for Table and Matrix Visuals
Table (tableEx) and Matrix (pivotTable) visuals are the primary target for DAX SVG measures. Configure grid.imageHeight and grid.imageWidth in visual objects to control rendering size (default: 25px height, 100px width).
Setup
Image Size Configuration
Set in the visual's objects.grid:
"grid": [{
"properties": {
"imageHeight": {"expr": {"Literal": {"Value": "25D"}}},
"imageWidth": {"expr": {"Literal": {"Value": "100D"}}}
}
}]Sort Trick
Embed a <desc> tag to enable sorting the SVG column by bar length:
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"Power BI uses the <desc> content as the sort key for the image column.
Axis Normalization (Required for All Bar-Based Charts)
All bar, bullet, and dumbbell charts need a shared axis maximum so bars are comparable across rows:
VAR _BarMax = 100 -- max pixel width of the bar area
VAR _BarMin = 20 -- left offset (space for labels/dots)
VAR _Scope = ALLSELECTED('Table'[GroupColumn])
VAR _MaxActual = CALCULATE(
MAXX(_Scope, [Actual]),
REMOVEFILTERS('Table'[GroupColumn])
)
VAR _MaxTarget = CALCULATE(
MAXX(_Scope, [Target]),
REMOVEFILTERS('Table'[GroupColumn])
)
VAR _AxisMax =
IF(
HASONEVALUE('Table'[GroupColumn]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([Actual], [Target]), REMOVEFILTERS('Table'[GroupColumn]))
) * 1.1 -- 10% headroom
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = (DIVIDE(_Target, _AxisMax) * _AxisRange) + _BarMin - 1Key points:
REMOVEFILTERSon the group column ensures_AxisMaxis consistent across all rows- Multiply by 1.1 for headroom so bars never hit the edge
- Target position = normalized value + left offset
Number Formatting (Adaptive Scale)
VAR _Label = SWITCH(TRUE(),
_Actual <= 1E3, FORMAT(_Actual, "#,0"),
_Actual <= 1E6, FORMAT(_Actual, "#,0, K"),
_Actual <= 1E9, FORMAT(_Actual, "#,0,, M"),
FORMAT(_Actual, "#,0,,, B")
)---
Pattern: Data Bar
Simple proportional bar for table columns. The most basic SVG pattern.
Data Bar =
VAR _Value = [Sales Amount]
VAR _Max = CALCULATE(MAX([Sales Amount]), REMOVEFILTERS('Product'[Category]))
VAR _Pct = DIVIDE(_Value, _Max)
VAR _W = _Pct * 100
VAR _Color = "#5B8DBE"
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 16'>" &
"<rect width='" & _W & "' height='16' fill='" & _Color & "' opacity='0.7' rx='2'/>" &
"<text x='" & (_W + 3) & "' y='12' font-size='10' fill='#333'>" &
FORMAT(_Value, "#,0") & "</text></svg>"Variants:
- Rounded corners: add
rx='4'to the rect - Rounded tops only: use
rxequal to half the height - Conditional color:
VAR _Color = IF(_Value > _Threshold, "#4CAF50", "#F44336")
---
Pattern: Bullet Chart with Action Dots
Combines actual bar, target line, baseline, and a sentiment-colored dot. From the SpaceParts production model.
SVG Bullet Chart =
-- Config
VAR _Actual = [MTD Turnover]
VAR _Target = [MTD Turnover 1YP]
VAR _Performance = DIVIDE(_Actual - _Target, _Target)
-- Sentiment thresholds
VAR _VeryBad = -0.05
VAR _Bad = -0.025
VAR _Good = 0.025
VAR _VeryGood = 0.05
-- Chart dimensions
VAR _BarMax = 100
VAR _BarMin = 20
VAR _Scope = ALL('Customers'[Key Account Name])
-- Colors
VAR _BackgroundColor = "#F5F5F5"
VAR _BarFillColor = "#CFCFCF"
VAR _BaselineColor = "#737373"
VAR _TargetColor = "#000000"
VAR _ActionDotFill =
SWITCH(TRUE(),
_Performance < _VeryBad, "#f4ae4c",
_Performance < _Bad, "#ffe075",
_Performance > _VeryGood, "#2D6390",
_Performance > _Good, "#74B2FF",
"#FFFFFF00"
)
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [MTD Turnover]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [MTD Turnover 1YP]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([MTD Turnover], [MTD Turnover 1YP]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = (DIVIDE(_Target, _AxisMax) * _AxisRange) + _BarMin - 1
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _ActionDot = "<circle cx='10' cy='11' r='5' fill='" & _ActionDotFill & "'/>"
VAR _BarBg = "<rect x='" & _BarMin & "' y='2' width='" & _BarMax & "' height='80%' fill='" & _BackgroundColor & "'/>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='5' width='" & _ActualNormalized & "' height='50%' fill='" & _BarFillColor & "'/>"
VAR _Baseline = "<rect x='" & _BarMin & "' y='4' width='1' height='60%' fill='" & _BaselineColor & "'/>"
VAR _TargetLine = "<rect x='" & _TargetNormalized & "' y='2' width='2' height='80%' fill='" & _TargetColor & "'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _ActionDot & _BarBg & _ActualBar & _Baseline & _TargetLine & _SvgSuffixKey elements:
- Action dot at left edge shows sentiment at a glance (colored circle)
- Background rect provides visual context for the bar area
- Target line as a thin rect (width 2px) at the target position
- Baseline at the left edge of the bar area
<desc>for sort ordering
---
Pattern: Overlapping Bars with Variance
Two bars (actual on top, target behind) with a colored variance indicator and label. From SpaceParts.
SVG Overlapping Bars =
-- Config
VAR _Actual = [Actuals MTD]
VAR _Target = [Budget MTD]
VAR _Performance = DIVIDE(_Actual - _Target, _Target)
-- Font
VAR _Font = "Segoe UI"
VAR _FontSize = 10
VAR _FontWeight = 600
-- Chart dimensions
VAR _BarMax = 100
VAR _BarMin = 30
VAR _Scope = ALLSELECTED('Customers'[Key Account Name])
-- Colors
VAR _ActualColor = "#686868"
VAR _TargetColor = "#e1dfdd"
VAR _VarianceColor = IF(_Performance < 0, "#fab005", "#2094ff")
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [Actuals MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [Budget MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([Actuals MTD], [Budget MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = DIVIDE(_Target, _AxisMax) * _AxisRange
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _Icon = "<text x='" & _BarMin - 3 & "' y='13.5' font-family='Segoe UI' font-size='6' font-weight='700' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT(_Performance, "^^^;vvv;") & "</text>"
VAR _Label = "<text x='" & _BarMin - 10 & "' y='15' font-family='" & _Font & "' font-size='" & _FontSize & "' font-weight='" & _FontWeight & "' text-anchor='end' fill='" & _VarianceColor & "'>" & FORMAT(_Performance, "#,##0%;#,##0%;#,##0%") & "</text>"
VAR _TargetBar = "<rect x='" & _BarMin & "' y='10' width='" & _TargetNormalized & "' height='12' stroke='" & _ActualColor & "' fill='" & _TargetColor & "'/>"
VAR _ActualBar = "<rect x='" & _BarMin & "' y='3' width='" & _ActualNormalized & "' height='12' stroke='" & _ActualColor & "' fill='" & _ActualColor & "'/>"
VAR _VarianceBar = "<rect x='" & _BarMin + MIN(_ActualNormalized, _TargetNormalized) + 1 & "' y='" & IF(_Target > _Actual, 2.9, 9) & "' width='" & ABS(_ActualNormalized - _TargetNormalized) - 1 & "' height='6' stroke='" & _VarianceColor & "' fill='" & _VarianceColor & "'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _Icon & _Label & _TargetBar & _ActualBar & _VarianceBar & _SvgSuffixKey elements:
- Variance label + directional icon at the left
- Target bar behind (wider height offset), actual bar on top
- Variance highlight bar spans the gap between actual and target
- Variance bar position (y) shifts based on whether actual > target
---
Pattern: Dumbbell Chart
Compares two values as circles connected by a line. From SpaceParts.
SVG Dumbbell Chart =
-- Config
VAR _Actual = [Actuals MTD]
VAR _Target = [Sales Target MTD]
-- Chart dimensions
VAR _SvgWidth = 100
VAR _SvgHeight = 25
VAR _Scope = ALLSELECTED('Customers'[Key Account Name])
-- Axis normalization
VAR _MaxActual = CALCULATE(MAXX(_Scope, [Actuals MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _MaxTarget = CALCULATE(MAXX(_Scope, [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
VAR _AxisMax =
IF(HASONEVALUE('Customers'[Key Account Name]),
MAX(_MaxActual, _MaxTarget),
CALCULATE(MAX([Actuals MTD], [Sales Target MTD]), REMOVEFILTERS('Customers'[Key Account Name]))
) * 1.1
VAR _AxisRange = _SvgWidth
VAR _ActualNormalized = DIVIDE(_Actual, _AxisMax) * _AxisRange
VAR _TargetNormalized = DIVIDE(_Target, _AxisMax) * _AxisRange
-- Colors (conditional: blue if on target, red if off)
VAR _AxisColor = "#C7C7C7"
VAR _Fill = IF(_Actual > _Target, "#448FD6", "#D64444")
VAR _Stroke = IF(_Actual > _Target, "#2F6698", "#982F2F")
-- SVG construction
VAR _SvgPrefix = "data:image/svg+xml;utf8,<svg width='" & _SvgWidth & "' height='" & _SvgHeight & "' xmlns='http://www.w3.org/2000/svg'>"
VAR _Sort = "<desc>" & FORMAT(_Actual, "000000000000") & "</desc>"
VAR _Axis = "<line x1='0' y1='" & _SvgHeight / 2 & "' x2='" & _SvgWidth & "' y2='" & _SvgHeight / 2 & "' stroke='" & _AxisColor & "'/>"
VAR _Origin = "<circle cx='2' cy='" & _SvgHeight / 2 & "' r='2' fill='" & _AxisColor & "'/>"
VAR _DumbbellLine = "<line x1='" & _ActualNormalized & "' y1='" & _SvgHeight / 2 & "' x2='" & _TargetNormalized & "' y2='" & _SvgHeight / 2 & "' stroke='" & _Fill & "' stroke-width='3'/>"
VAR _TargetCircle = "<circle cx='" & _TargetNormalized & "' cy='" & _SvgHeight / 2 & "' r='4' fill='#F5F5F5' stroke='#C7C7C7' stroke-width='1.5'/>"
VAR _ActualCircle = "<circle cx='" & _ActualNormalized & "' cy='" & _SvgHeight / 2 & "' r='4' fill='" & _Fill & "' stroke='" & _Stroke & "' stroke-width='1.5'/>"
VAR _SvgSuffix = "</svg>"
RETURN
_SvgPrefix & _Sort & _Axis & _Origin & _DumbbellLine & _TargetCircle & _ActualCircle & _SvgSuffixKey elements:
- Horizontal axis line spanning the full width
- Origin dot at left edge
- Connecting line between actual and target (colored by performance)
- Target circle: neutral grey fill, grey stroke
- Actual circle: blue (on target) or red (off target)
- Render order matters: line first, then target circle, then actual circle on top
---
Pattern: Status Pill
Colored pill with text label for categorical status. From MacGuyver Toolbox.
Status Pill =
VAR _Status = [Status Category]
VAR _BgColor = SWITCH(_Status,
"On Track", "#CEE5D0",
"At Risk", "#FFF3CD",
"Late", "#EBD2CE",
"#F0F0F0"
)
VAR _TextColor = SWITCH(_Status,
"On Track", "#2D6A2E",
"At Risk", "#856404",
"Late", "#982F2F",
"#333333"
)
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>" &
"<rect x='0.5' y='0.5' width='98%' height='95%' rx='15%' fill='" & _BgColor & "' stroke='" & _TextColor & "'/>" &
"<text x='50%' y='58%' font-family='Segoe UI' font-size='12' font-weight='700' fill='" & _TextColor & "' text-anchor='middle' dominant-baseline='middle'>" & UPPER(_Status) & "</text></svg>"---
Pattern: Lollipop Chart
Thin line with proportionally sized dot at the end. From MacGuyver Toolbox.
Lollipop Chart =
VAR _Actual = [Sales Amount]
VAR _Target = [Sales Target]
VAR _BarMax = 75
VAR _BarMin = 20
VAR _Scope = ALL('Product'[Category])
VAR _MaxVal = CALCULATE(MAX([Sales Amount], [Sales Target]), REMOVEFILTERS('Product'[Category])) * 1.1
VAR _AxisRange = _BarMax - _BarMin
VAR _ActualNormalized = DIVIDE(_Actual, _MaxVal) * _AxisRange
VAR _DotRadius = MAX(DIVIDE(_Actual, _MaxVal) * 7.5, 3.5)
VAR _Performance = DIVIDE(_Actual - _Target, _Target)
VAR _Color = IF(_Performance >= 0, "#448FD6", "#D64444")
VAR _Label = SWITCH(TRUE(),
_Actual <= 1E3, FORMAT(_Actual, "#,0"),
_Actual <= 1E6, FORMAT(_Actual, "#,0, K"),
FORMAT(_Actual, "#,0,, M")
)
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>" &
"<desc>" & FORMAT(_Actual, "000000000000") & "</desc>" &
"<rect x='" & _BarMin & "' y='12' width='" & _ActualNormalized & "' height='1.5' fill='" & _Color & "'/>" &
"<circle cx='" & (_BarMin + _ActualNormalized) & "' cy='12.5' r='" & _DotRadius & "' fill='" & _Color & "'/>" &
"<text x='" & _BarMin - 3 & "' y='15' font-family='Segoe UI' font-size='9' font-weight='600' text-anchor='end' fill='" & _Color & "'>" & _Label & "</text></svg>"Key elements:
- Dot radius scales proportionally with value:
MAX(DIVIDE(_Actual, _MaxVal) * 7.5, 3.5) - Thin line (height 1.5px) connects the left edge to the dot
- Label positioned to the left of the bar area
---
Pattern: Sparkline (Polyline)
Line sparkline showing trend over time using CONCATENATEX.
Sparkline SVG =
VAR _Values = ADDCOLUMNS(
CALCULATETABLE(
VALUES('Date'[Month]),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)
),
"@Value", [Sales Amount]
)
VAR _XMin = MIN('Date'[Month])
VAR _XMax = MAX('Date'[Month])
VAR _YMin = MINX(_Values, [@Value])
VAR _YMax = MAXX(_Values, [@Value])
VAR _Points = CONCATENATEX(
ADDCOLUMNS(_Values,
"@X", INT(100 * DIVIDE('Date'[Month] - _XMin, _XMax - _XMin)),
"@Y", INT(30 * DIVIDE([@Value] - _YMin, _YMax - _YMin))
),
[@X] & "," & (30 - [@Y]),
" ",
'Date'[Month]
)
RETURN
IF(HASONEVALUE('Product'[Category]),
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 30'>" &
"<polyline fill='none' stroke='#5B8DBE' stroke-width='2' points='" & _Points & "'/></svg>",
BLANK()
)Key technique: Y is inverted (30 - [@Y]) because SVG Y=0 is at the top.
---
Pattern: Area Sparkline with Gradient
Area Sparkline =
VAR Defs = "<defs><linearGradient id='grad' x1='0' y1='0' x2='0' y2='50' gradientUnits='userSpaceOnUse'><stop stop-color='navy' offset='0'/><stop stop-color='transparent' offset='1'/></linearGradient></defs>"
VAR XMin = MIN('Table'[Date])
VAR XMax = MAX('Table'[Date])
VAR YMin = MINX(VALUES('Table'[Date]), CALCULATE([Measure]))
VAR YMax = MAXX(VALUES('Table'[Date]), CALCULATE([Measure]))
VAR Points = ADDCOLUMNS(
SUMMARIZE('Table', 'Table'[Date]),
"X", INT(150 * DIVIDE('Table'[Date] - XMin, XMax - XMin)),
"Y", INT(50 * DIVIDE([Measure] - YMin, YMax - YMin))
)
VAR Lines = CONCATENATEX(Points, [X] & "," & (50 - [Y]), " ", 'Table'[Date])
RETURN IF(HASONEVALUE('Table'[Category]),
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 50'>" &
Defs &
"<polyline fill='url(#grad)' fill-opacity='0.3' stroke='navy' stroke-width='3' points='0 50 " & Lines & " 150 50 Z'/></svg>",
BLANK())---
Pattern: Bar Sparkline
Bar Sparkline =
VAR _Values = ADDCOLUMNS(
CALCULATETABLE(VALUES('Date'[Month]),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)),
"@Value", [Sales Amount]
)
VAR _Count = COUNTROWS(_Values)
VAR _Max = MAXX(_Values, [@Value])
VAR _Min = MINX(_Values, [@Value])
VAR _Range = _Max - _Min
VAR _W = 100
VAR _H = 30
VAR _BarW = _W / _Count
VAR _Scale = _H / _Range
VAR _Bars = CONCATENATEX(
ADDCOLUMNS(_Values, "@Idx", RANKX(_Values, 'Date'[Month], , ASC)),
VAR _X = ([@Idx] - 1) * _BarW
VAR _BarH = ([@Value] - _Min) * _Scale
VAR _Y = _H - _BarH
RETURN "<rect x='" & _X & "' y='" & _Y & "' width='" & (_BarW * 0.8) &
"' height='" & _BarH & "' fill='#2196F3' opacity='0.7'/>",
"", [@Idx], ASC
)
RETURN
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 " & _W & " " & _H & "'>" & _Bars & "</svg>"---
UDF Pattern
For reusable SVG charts, use DAX UDFs (User-Defined Functions) that accept parameters. These are defined as extension measures in a dedicated __SVGs or similar table.
-- Calling a UDF
SVG.Chart.BulletChart.ActionDot(
[MTD Turnover], -- Actual measure
[MTD Turnover 1YP], -- Target measure
'Customers'[Key Account Name], -- Group column
-0.050, -- Very bad threshold
-0.025, -- Bad threshold
0.025, -- Good threshold
0.050, -- Very good threshold
"#f4ae4c", -- Very bad color
"#ffe075", -- Bad color
"#74B2FF", -- Good color
"#2D6390" -- Very good color
)Available UDF libraries:
SVG.Chart.BarChart.OverlappingBars-- overlapping bars with variance labelSVG.Chart.BarChart.OverlappingBarsSimple-- overlapping bars without labelSVG.Chart.BulletChart.ActionDot-- bullet chart with sentiment dotsSVG.Chart.DumbbellPlot-- dumbbell comparison chartSVG.Chart.Waterfall-- waterfall chart with connectors
See the PowerBI MacGuyver Toolbox and DaxLib.SVG libraries for more UDFs.
---
Performance in Table and Matrix Cells
An SVG measure is a string-building DAX expression evaluated once per visible cell (per row in a table; per row x column intersection in a matrix), all in the single-threaded formula engine with no storage-engine acceleration for the string concatenation. Cost scales with iteration count, not byte size.
Pre-aggregate in model measures, not inside the SVG string
A sparkline that runs CONCATENATEX over 24 monthly points for every row is ~720 point-iterations of string assembly per page refresh, re-evaluated on every cross-filter. Push the base aggregations into reusable model measures or a precomputed table so the storage-engine cache absorbs that work; the SVG measure should only map computed numbers to coordinates.
Prefer one <polyline> over N individual elements
A single CONCATENATEX producing a points='...' string for <polyline> iterates once and produces one element. Replacing it with N <circle> or <line> elements runs N iterations and emits N elements for the parser. Use <polyline> for line/area sparklines; reserve individual shapes for endpoints or markers only.
Round coordinates to integers
Shorter strings mean cheaper FORMAT calls and less markup:
-- Preferred: integer coordinates
VAR _X = INT(DIVIDE(_Month - _XMin, _XMax - _XMin) * 100)
-- Avoid: floating-point strings like "47.3829..."The 32K limit is per rendered cell, not per expression
The ceiling applies to the string returned by the measure for each individual cell. Diagnose by running LEN([Your SVG Measure]) via pbir model -q for a worst-case category member. Near the ceiling, the cell silently drops to blank text. Options when approaching the limit:
- Reduce the time-series window (12 months instead of 24)
- Split into two simpler measures rendered in adjacent columns
- Move the visualization to Deneb, which has no comparable string ceiling
The HASONEVALUE/ISINSCOPE total guard matters for both correctness and cost
A matrix evaluates SVG measures at subtotal and grand-total rows too. Without a guard, the measure runs at a coarser grain and may return a meaningless or oversized SVG. Gate explicitly:
IF(
HASONEVALUE('Product'[Category]),
-- SVG assembly
BLANK()
)For nested matrices, ISINSCOPE('Product'[SubCategory]) targets a specific hierarchy level and avoids building SVGs at every subtotal band.
Caching and volatile inputs
Identical SVG strings for identical filter contexts hit the formula engine result cache. Inputs that change every evaluation defeat caching: NOW(), TODAY(), and unseed random jitter. The jitter-plot example uses a hash-based pseudo-random from a key column; preserve that pattern rather than introducing RAND().
grid.imageHeight/grid.imageWidth and column width
Setting large image dimensions in the visual's objects.grid does not change the formula-engine cost of generating the SVG, but it adds rasterization and paint cost in the renderer. Keep dimensions proportional to the cell content; avoid inflating them for visual effect. Auto-size-width ON with a wide SVG column triggers horizontal scroll and re-layout churn; set an explicit columnWidth in the visual formatting.