BarFree

Multi-Tier Bar Charts (SVG Version)

Improved version:

Written byIwa Sanjaya
Updated on30 December 2025Read time42 min

Multi-Tier Bar Charts (SVG Version)

Improved version:

Foreword

Scalable Vector Graphics (SVGs) offer greater flexibility for implementing IBCS Notation than native Power BI visuals. While multi-tier bar charts can be built natively using three separate bar charts, this approach faces limitations. Specifically, the third tier, which displays the relative variance, often contains extreme outliers. If these are not truncated, the smaller variance values become difficult to compare. Although achieving the desired result with native visuals is possible, it typically requires significant workarounds. Therefore, using SVGs is the proposed alternative to effectively manage this issue.

The original User Defined Function (UDF) codes required to generate the SVG bar charts were contributed by Andrzej Leszkiewicz and are accessible for download at the following link:

DAX Lib

These codes were adapted to align with the specific IBCS notations required for this visual.

About Multi-Tier Bar Charts

Multi-Tier Bar Charts (source: IBCS)

Multi-Tier Bar Charts consist of three tiers structure to analyze data across different scenarios. The first tier presents a grouped bar chart for scenario comparison, while the second and third tiers display absolute and relative variance charts, respectively. For best results, use this template when presenting two scenarios and limiting the content to 25 structure elements or less. To include a third scenario, the data must be displayed using scenario triangles.

Guide to IBCS Notation in this Visual (Semantic Rules)

This section explains the IBCS principles specifically implemented in this visual. The complete IBCS guidelines are available for free download on the IBCS website upon registration.

IBCS Standards 1.2 (pdf) - IBCS.com

Development of a dedicated summary of the IBCS Standards is currently in progress.


UN 3.2 UNIFY SCENARIOS

Scenarios (also known as data categories or versions) represent different layers or assumptions within a business model. Typical scenarios include Actual, Previous Year (PY), Plan, Budget, and Forecast. Benchmarks like competitor data can also be considered scenarios. Reporting often involves showing comparisons and variances between these scenarios to provide business insights.

Three Types of Scenarios

IBCS classifies scenarios into three basic types, visually distinguished by how their visualization element (bar, column, etc.) is filled:

TypeDescriptionTypical Terms
ActualData about things that have already happened.'Actual' (AC), 'Previous Year' (PY)
PlannedData that is not yet materialized or measured.'Plan' (PL), 'Budget' (BU)
ForecastedData that is fictitious but incorporates measured data (e.g., a sales forecast based on measured order entry).'Forecast' (FC)

Figure UN 3.2: Unify scenarios — Source: IBCS Standards 1.2


UN 3.3: UNIFY TIME PERIODS & USE HORIZONTAL AXES

Consistent notation for time periods (for flow measures like sales) and points of time (for stock measures like inventory) is vital in all business communication. This requires standards for the visual direction of time, time abbreviations, and category widths in charts.

Visual Direction of Time Periods

Unlike structural comparisons, data series that represent change over time should always be visualized using horizontal axes.

  • In charts, time should progress from left to right.

  • In tables, time series data should also be presented in columns where time moves from left to right.

Figure UN 3.3-1: Visualization of time vs. structure (examples) — Source: IBCS Standards 1.2


UN 5.3 UNIFY OUTLIER INDICATORS

Sometimes in a chart, one value is much higher or lower than the others. This is called an outlier. If this outlier is not important for the business (for example, a very high percentage change but based on a very small amount), we should not stretch the whole chart just to include it. Instead, we can keep the chart scale based on the important values and show the outlier using a special indicator — such as a small triangle pointing in the direction of increase or decrease. This keeps the chart clear and easy to read. As shown in Figure UN 5.3, it’s better to remove the pin head and use a triangle to show the outlier. This keeps the design clean and consistent.

Figure UN 5.3: Unify outlier indicators — Source: IBCS Standards 1.2

Documentation

IBCS Multi-Tier Bar Charts

The multi-tier bar charts consist of three distinct tiers, each serving a specific analytical purpose:

  1. Grouped Bar Chart (First Tier): A clustered bar chart that displays the current (Actual/AC) versus previous year's (Prior Year/PY) values side-by-side.

  2. Absolute Variance Chart (Second Tier): A diverging bar chart illustrating the absolute difference between AC and PY. Green signifies positive variance (desirable), and red signifies negative variance (undesirable).

  3. Relative Variance Chart (Third Tier): A "pin chart," displaying the relative variance between AC and PY. The use of thin bars emphasizes that relative variance is a percentage and lacks absolute volume, unlike the values in the first and second tiers.

These charts strictly adhere to the International Business Communication Standards (IBCS) principles, ensuring effective and standardized business reporting.


Understanding the Logic to Display Absolute and Relative Variances for Each State

For this sales performance case study (net sales by region and state), the logic for displaying absolute and relative variances for each state must be adjusted based on whether that state has sales values for the current year (AC), the previous year (PY), or both.

No.ScenarioAC (Current Selected Year)PY (Previous Year)Absolute Variance (△PY)Relative Variance (△PY%)Display Behavior
1.Normal ComparisonHas ValueHas ValueAC - PY(AC - PY) / PYShow all columns
2.New StateHas ValueBlank+AC (full value)+100%Show all, indicate growth
3.Closed StateBlankHas Value-PY (negative)-100%Show all, indicate closure
4.No DataBlankBlankBlankBlankHide state entirely

Three panels: prior-year and actual bars for four states, an absolute variance column and a percentage variance column, where State 3 reads -100% for a closure and State 4 is blank

  • Scenario 1: When Both Current and Previous Year Data Exist (AC and PY exist) → The DAX calculates normal variances. For example, if California had 131.6 kUSD in current year sales and 88.5 kUSD last year, it shows the difference (+43.1 kUSD) and the percentage change (+48.7%). This appears as green bars because sales increased.

  • Scenario 2: When a State is New (Has Current Year Sales But No Previous Year) → The DAX treats this as a new business situation. It shows the full current year amount as the variance and displays +100% for the percentage. For example, Wyoming shows +1.6 kUSD in absolute variance and +100.0% because it had 1.6 kUSD this year but no sales last year. This makes sense because the business went from nothing to something.

  • Scenario 3: When a State Closed (Has Previous Year Sales But No Current Year) → The DAX treats this as a complete loss. It shows the negative of the previous year amount and displays -100%. For example, Maine shows -0.6 kUSD and -100.0% because it had sales last year but none this year.

  • Scenario 4: When a State Has No Data at All → If a state has neither current year nor previous year sales, the DAX hides that state completely from the chart There's nothing to analyze, so there's no point showing it.


Step 1: Defining the Prerequisite Measures

For this scenario, the multi-tier bar charts will initially display only one measure: net sales. However, you are welcome to change this to any other metric (e.g., transactions, quantity) or even set up a dynamic metric controlled by a slicer.

Prerequisite Measure #01: Total Net Sales

DAX
_01 Total Net Sales = SUM(Superstore[Sales]) / 1000

The main reason for dividing the value by 1,000 is to optimize chart readability. By presenting the net sales as a condensed figure (e.g., "1.2") rather than the full number ("1,234"), we ensure concise data labels and avoid truncation issues on the visualization.

Prerequisite Measure #02: PY Net Sales

DAX
_02 PY Net Sales = 
IF(
  HASONEVALUE(DimDate[Year]),
  CALCULATE(
      [_01 Total Net Sales],
      SAMEPERIODLASTYEAR(DimDate[Date])
  ),
  BLANK()
)

This measure calculates the Net Sales from the same period last year — but only if one year is currently selected. This makes it safe and accurate for year-over-year (YoY) analysis.

Read the Explanation

Step-by-Step Explanation

1. IF condition — ensure only one year is selected

DAX
IF(
  HASONEVALUE(DimDate[Year]),
  • HASONEVALUE(DimDate[Year]) checks whether exactly one year is selected in the current filter context.

  • This prevents DAX from giving wrong results when multiple years are visible at once (like in a multi-year table).


2. CALCULATE with SAMEPERIODLASTYEAR

DAX
CALCULATE(
  [_01 Net Sales],
  SAMEPERIODLASTYEAR(DimDate[Date])
)

This is the core logic. Let’s understand both parts:

  • [_01 Net Sales]: your base measure for total Net Sales (current period).

  • CALCULATE(...): modifies the filter context to look at a different period (previous year).

  • SAMEPERIODLASTYEAR(DimDate[Date]): shifts the current date filter exactly one year back.

So if your current visual is showing: Jan–Feb 2024 → this part of the formula will pull Jan–Feb 2023 sales.


3. Else part — return BLANK

DAX
,
BLANK()
)

If more than one year is selected, the measure safely returns nothing (BLANK()), to avoid confusing or doubled results.

Prerequisite Measure #03: △PY Net Sales

DAX
_03 △PY Net Sales = 
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]
RETURN
  SWITCH(
      TRUE(),
      -- Both blank: no variance
      ISBLANK(_AC) && ISBLANK(_PY), BLANK(),
      
      -- AC exists, PY blank: full AC as variance (new state)
      NOT(ISBLANK(_AC)) && ISBLANK(_PY), _AC,
      
      -- AC blank, PY exists: negative PY as variance (closed state)
      ISBLANK(_AC) && NOT(ISBLANK(_PY)), -_PY,
      
      -- Both exist: normal calculation
      _AC - _PY
  )

This measure compares Net Sales of the current year (AC) with Net Sales of the previous year (PY) and calculates the difference / variance. It handles all possible situations when comparing current year vs previous year sales. It identifies new entries, discontinued entries, and calculates growth or decline when data exists for both years.

Read the Explanation

Logic Breakdown

The logic checks 4 different situations:

CaseSituationWhat It MeansResult
1Both AC and PY are blankNo sales data for both yearsShow blank
2AC has value, PY is blankNew state – first time sales appearShow full AC value
3AC is blank, PY has valueState is gone / discontinuedShow negative PY
4Both AC and PY existNormal comparisonAC – PY

Step-by-Step Explanation

1. Define Variables

DAX
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]

Store current year and previous year’s net sales in variables.


2. Compare Situations

DAX
SWITCH(TRUE(),

SWITCH(TRUE()) lets us check multiple conditions easily.


3. Handle Each Situation

Case 1 – No Data for Both Years

DAX
ISBLANK(_AC) && ISBLANK(_PY), BLANK(),

If there’s no data for both years → show nothing.


Case 2 – AC Exists, PY is Blank

DAX
NOT(ISBLANK(_AC)) && ISBLANK(_PY), _AC,

If there's sales in current year, but none last year → That means new sales entry → show full AC value.


Case 3 – AC is Blank, PY Exists

DAX
ISBLANK(_AC) && NOT(ISBLANK(_PY)), -_PY,

If no sales this year, but there were sales last year → That state/city/product likely stopped selling → show negative PY.


Case 4 – Normal Comparison

DAX
_AC - _PY

Both years have sales → calculate variance normally.

Prerequisite Measure #04: △PY% Net Sales

DAX
_04 △PY% Net Sales = 
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]
RETURN
  SWITCH(
      TRUE(),
      -- Both blank: no comparison
      ISBLANK(_AC) && ISBLANK(_PY), BLANK(),
      
      -- AC exists, PY blank: treat as +100% (new state)
      NOT(ISBLANK(_AC)) && ISBLANK(_PY), 1,
      
      -- AC blank, PY exists: -100% (closed state)
      ISBLANK(_AC) && NOT(ISBLANK(_PY)), -1,
      
      -- Both exist: normal calculation
      DIVIDE(_AC - _PY, _PY, BLANK())
  )

This DAX calculates the percentage change of Net Sales compared to last year — also known as % variance vs PY. But this measure also handles special cases (new or closed states) intelligently.

Read the Explanation

Step-by-Step Logic

1. Define variables

DAX
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]

Store current year (AC) and previous year (PY) net sales.


2. Check Different Scenarios Using SWITCH(TRUE())

CaseSituationMeaningResult
1Both AC & PY are blankNo dataShow blank
2AC exists, PY is blankNew entry this year+100% (1)
3AC blank, PY existsStopped selling100% (-1)
4Both existStandard % change(AC − PY) / PY

3. DAX Logic Explanation

Case 1 – No Data

DAX
ISBLANK(_AC) && ISBLANK(_PY), BLANK(),

No data for both years → no comparison → return BLANK().


Case 2 – AC Exists, PY Blank

DAX
NOT(ISBLANK(_AC)) && ISBLANK(_PY), 1,

Sales only appear this year → new state / new customer → So it’s treated as **+100% growth (**represented as 1).


Case 3 – AC Blank, PY Exists

DAX
ISBLANK(_AC) && NOT(ISBLANK(_PY)), -1,

Sales disappear this year → lost customer / stopped selling → So it’s treated as 100% decline.


Case 4 – Normal % Calculation

DAX
DIVIDE(_AC - _PY, _PY, BLANK())

Standard formula: (AC – PY) / PY. Using DIVIDE() prevents error if PY = 0.

Step 2: Defining UDFs to Display the SVG Bar Charts

We will now define the User Defined Functions (UDFs) for generating the tiers of the multi-tier bar charts. We based these UDFs on the original code by Andrzej Leszkiewicz, making slight adjustments to achieve the proper results for this case study.

UDF #01: Grouped Bar Chart (First Tier)

DAX
DEFINE
  FUNCTION PowerofBI.IBCS.BarChart.AbsoluteValues = /// Creates a bar chart (SVG-image) that compares absolute values (actual value vs base value)
/// @param {scalar} valueMain
/// @param {scalar} valueSecond
/// @param {scalar} valueBase
/// @param {string} baseType
/// @param {string} dataLabel
/// @param {voolean} isTotalRow
/// @param {scalar} valueMax
/// @returns SVG image
(
  // Main value (actual)
  valueMain: SCALAR VAL, 
  // Secondary value (forecasted)
 valueSecond: SCALAR VAL,
 // Base value (previous year)
 valueBase: SCALAR VAL,
 // Base value type: "grey" or "outlined"
 baseType: STRING VAL,
 // Data (bar) label
 dataLabel: STRING VAL,
 // Is it a total row? - to treat total rows differently
 isTotalRow: BOOLEAN VAL,
 // Max of main, secondary and base values for all items in the visual (ALLSELECTED) - for scaling
 valueMax: SCALAR VAL
)
=>
//=================================================================================================
//Project: an UDF for generating IBCS-styled charts (as SVG images)
//  The visualization design follows IBCS guidelines https://www.ibcs.com/ibcs-standards-1-2/
//  Although it is not produced, not certified, not authorized, not endorsed by IBCS® https://www.ibcs.com/
//  The charts can be embedded into Table, Matrix, and New Card core Power BI visuals
//=================================================================================================
//Author: Andrzej Leszkiewicz, an IBCS® Certified Analyst
//  For more information, visit https://powerofbi.org/
//  Connect on LinkedIn at https://www.linkedin.com/in/avatorl/
//  Contact via email at andrzej@powerofbi.org
//=================================================================================================
//License: MIT Expat License https://en.wikipedia.org/wiki/MIT_License (free)
//=================================================================================================
//Description: This UDF Generates an SVG image featuring a bar chart (to show absolute values)
//  The chart includes a base value bar: grey for the Previous Year (PY) or outlined for the Budget (BU)
//  The main and second value stacked bars are represented with the main value (AC, actuals) in solid black and the second value (FC, forecast) hatched
//  It also includes data labels for and reference value triangles: outlined for the Budget (BU) or in grey  the Previous Year (PY)
//=================================================================================================    
//CONFIG: chart formatting options
//=================================================================================================//
VAR _SVG_Width = 400
VAR _FontSize = 16
VAR _SVG_LeftPadding =  5 --left side padding (before Y axis)
VAR _SVG_RightPadding = 355 --right side padding (space for data labels)
VAR _ColorMain = "#404040"
VAR _ColorSecond = "#A6A6A6"
VAR _Rank =
  1000000000000 + ROUND ( valueMain, 0 ) --a value that will be used to sort the column (main value converted into a sortable string)
VAR _ColorBase =
  SWITCH ( baseType, "grey", _ColorSecond, "outlined", "#FFFFFF" ) --base value columns are either grey or white with black outline
VAR _ColorBaseOutline =
  SWITCH ( baseType, "grey", _ColorSecond, "outlined", _ColorMain ) --base value columns are either grey or white with black outline
VAR _Em =
  ROUND ( _FontSize * 4 / 3, 0 ) --convert Pt font size to Px font size
VAR _AxisLineWidth = _Em * 0.1
VAR _mainBarYPosition = _Em * 0.25
VAR _LabelOffset = 5 --offset between bars and data labels
VAR _FontWeight =
  --bold font for the total row
  IF ( isTotalRow, "bold", "normal" ) //
// set image width (Format Pane) to [SVG Width]
// set image height (Format Pane) to _Em*1.6 (=36 for Font Size 18)  
//=================================================================================================
//SCALING: converting data values into X and Y positions on the SVG plot
//=================================================================================================
VAR _SVG_ColumnWidth = _SVG_Width - _SVG_LeftPadding - _SVG_RightPadding
VAR _Scale =
  DIVIDE ( _SVG_ColumnWidth, valueMax ) -- how many pixels per value unit
VAR _WidthMain =
  ROUND ( valueMain * _Scale, 0 ) --Main bar width (length)
VAR _WidthSecond =
  ROUND ( valueSecond * _Scale, 0 ) --Second bar width (length), stacked with the main bar
VAR _WidthBase =
  ROUND ( valueBase * _Scale, 0 ) --Base bar width (length)    
//
//=================================================================================================    
//GENERATE SVG: concatenating parts of the SVG image
//=================================================================================================
VAR _SVG_Header = "data:image/svg+xml;utf8,"
VAR _SVG_Open = "<svg xmlns='http://www.w3.org/2000/svg' width='" & _SVG_Width & "' >"
VAR _SVG_Close = "</svg>"
VAR _SVG_Style = "<style>
    text{
      font-family: Inter, sans-serif;
      font-size: " & _Em & "px;
      font-weight: " & _FontWeight & ";
      dominant-baseline: central;
    }
</style>"
VAR SVG_HatchedPattern =
  "/*hatched pattern*/
  <defs >
  <pattern id='diagonal-stripe' patternUnits='userSpaceOnUse' width='8' height='8'>
      /*white background*/<rect x='0' y='0' width='8' height='8' fill='#FFFFFF' stroke-width='0'/>
      /*black stripes*/
      <path d='
          M-2,2 l4,-4
          M0,8 l8,-8
          M6,10 l4,-4' 
      style='stroke:black; stroke-width:2' />
  </pattern>
  </defs>"
VAR _SVG_SortBy = "/*SortBy:" & _Rank & "*/"
VAR _SVG_Box = "" --"<rect x='0%' y='0%' width='100%' height='100%' fill='#FFAAAA' />"
VAR _SVG_LineAxisY = "<line id='line-axisY'  x1='" & _SVG_LeftPadding & "px' x2='" & _SVG_LeftPadding & "' y1='0%' y2='100%' stroke='#000000' stroke-width='" & _AxisLineWidth & "'></line >"
VAR _SVG_BarBase = "<rect id='rect-base-value' x='" & _SVG_LeftPadding & "px' width='" & _WidthBase & "' y='0' height='" & _Em & "' fill='" & _ColorBase & "' stroke='" & _ColorBaseOutline & "' stroke-width='1'></rect>"
VAR _SVG_BarMain = "<rect id='rect-main-value' x='" & _SVG_LeftPadding & "px' width='" & _WidthMain & "' y='" & _mainBarYPosition & "' height='" & _Em & "' fill='" & _ColorMain & "' stroke='" & _ColorMain & "' stroke-width='1.5'></rect>" //
VAR _SVG_BarSecond = "<rect x='" & _SVG_LeftPadding + _WidthMain & "px' width='" & _WidthSecond & "' y='" & _Em * 0.25 & "' height='" & _Em & "' fill='" & "url(#diagonal-stripe)" & "' stroke='" & _ColorMain & "' stroke-width='0.5'></rect>" //
VAR _SVG_TextACFCLabel = "<text x='" & _SVG_LeftPadding + _WidthMain + _WidthSecond & "' dx='" & _LabelOffset & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_TextACFCLabelTotal = "<text x='" & _SVG_LeftPadding & "' dx='" & 0 & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_Total = _SVG_Header & _SVG_Open & _SVG_TextACFCLabelTotal & _SVG_Style & _SVG_Close
VAR _SVG_Row = _SVG_Header & _SVG_Open & _SVG_SortBy & _SVG_Box & SVG_HatchedPattern & _SVG_BarBase & _SVG_BarMain & _SVG_BarSecond & _SVG_LineAxisY & _SVG_TextACFCLabel & _SVG_Style & _SVG_Close //
VAR _SVG =
  IF ( isTotalRow, _SVG_Total, _SVG_Row )
RETURN 
  _SVG

EVALUATE
  {
      PowerofBI.IBCS.BarChart.AbsoluteValues()
  }

This is a User-Defined Function (UDF) in DAX that generates an IBCS-style bar chart as an SVG image inside Power BI — so you can embed it into a table, matrix, or card visual.

Read the Explanation

What This Function Does

It creates a bar chart in SVG format that shows:

ElementMeaning
Base bar (gray or outlined)Previous Year (PY) or Budget (BU)
Main barActuals (AC)
Hatched barForecast (FC) → stacked beside AC
Text labelShows the data label next to bar
Total rows appear differentlyText only (no bars)
Scales bar size based on max valueSo all rows fit proportionally

Inputs/Parameters

ParameterWhat It Means
valueMainActual value (AC) → solid black bar
valueSecondForecast (FC) → hatched bar
valueBasePY or BU → gray or outlined bar
baseType"grey" = PY, "outlined" = budget
dataLabelText label shown next to the bar
isTotalRowIf TRUE → only show label (no bars)
valueMaxThe maximum among all values 👉 used for scaling (important!)

valueMax must come from MAXX(ALLSELECTED(...)), so that all bars are drawn fairly and proportionally.


Main Concept: Scaling the Bars

To convert values into pixel widths:

DAX
_SVG_ColumnWidth = _SVG_Width - _SVG_LeftPadding - _SVG_RightPadding

_Scale = DIVIDE( _SVG_ColumnWidth, valueMax )
_WidthMain  = ROUND ( valueMain  * _Scale, 0 )   -- AC bar
_WidthSecond = ROUND ( valueSecond * _Scale, 0 ) -- FC bar
_WidthBase  = ROUND ( valueBase  * _Scale, 0 )   -- PY/BU bar

Meaning: if valueMax = 100 and chart width is 300px → every 1 sales unit = 3px width. That keeps the bars visually consistent.


How the Chart Is Drawn

SVG is just text + shapes. The function builds the image using DAX strings:

Base Bar (Previous Year / Budget)

DAX
_SVG_BarBase =
"<rect x='5' width='100' height='20' fill='#A6A6A6' ... ></rect>"

Main Bar (Actuals)

DAX
_SVG_BarMain =
"<rect x='5' width='150' height='20' fill='#404040'></rect>"

Second Bar (Forecast – hatched!)

DAX
_SVG_BarSecond =
"<rect fill='url(#diagonal-stripe)' ... ></rect>"

Label

DAX
_SVG_TextACFCLabel =
"<text x='200' y='50%'>AC 150 | FC 30</text>"

Total Rows Are Treated Differently

DAX
VAR _SVG =
  IF ( isTotalRow, _SVG_Total, _SVG_Row )

✔ If it's a subtotal or total row → ❌ bars are not drawn → 🟢 only the label is shown (bold font). This avoids cluttering totals visually.


Bonus: How to Call the Function

Example usage:

DAX
_Chart =
PowerofBI.IBCS.BarChart.AbsoluteValues (
  [_AC],
  [_FC],
  [_PY],
  "grey",
  FORMAT([_AC], "#,##0"),
  FALSE(),
  [_Max]
)

This function converts your AC, PY, and FC values into a fully IBCS-compliant bar chart using SVG — ready to be shown directly inside Power BI tables/matrix visuals.

UDF #02: Absolute Variance Chart (Second Tier)

DAX
DEFINE
  FUNCTION PowerofBI.IBCS.BarChart.AbsoluteVariance = /// Creates a bar chart that shows the absolute variance between two values
/// @param {scalar} valueMain
/// @param {string} dataLabel
/// @param {boolean} isTotalRow
/// @param {scalar} valueMax
/// @param {scalar} valueMin
/// @param {scalar} valueScale
/// @returns SVG image
(
  // Absolute variance = actual value - base (previous year) value
  valueMain: SCALAR VAL,
  // Data label - formatted variance value. Use FORMAT()
  dataLabel: STRING VAL,  
  // Is it a total row? Use NOT ( ISINSCOPE () )
  isTotalRow: BOOLEAN VAL, 
  // Max of the variance for all items in the table (ALLSELECTED)
  valueMax: SCALAR VAL,  
  // Min of the variance for all items in the table (ALLSELECTED)
  valueMin: SCALAR VAL,    
  // Max of the both (main and base) absolute values for all items in the table (ALLSELECTED)
  valueScale: SCALAR VAL
)
=> 
//=================================================================================================
//Project: an UDF for generating IBCS-styled charts (as SVG images)
//  The visualization design follows IBCS guidelines https://www.ibcs.com/ibcs-standards-1-2/
//  Although it is not produced, not certified, not authorized, not endorsed by IBCS® https://www.ibcs.com/
//  The charts can be embedded into Table, Matrix, and New Card core Power BI visuals
//=================================================================================================
//Author: Andrzej Leszkiewicz, an IBCS® Certified Analyst
//  For more information, visit https://powerofbi.org/
//  Connect on LinkedIn at https://www.linkedin.com/in/avatorl/
//  Contact via email at andrzej@powerofbi.org
//=================================================================================================
//License: MIT Expat License https://en.wikipedia.org/wiki/MIT_License (free)
//=================================================================================================
//Description: This UDF Generates an SVG image featuring a diverging bar chart (to show absolute variance)
//  The chart includes diverging bars value bar: green for positive variance, red for negative variance
//  It also includes data labels (for all bars)
//=================================================================================================    
//CONFIG: chart formatting options
//=================================================================================================//
VAR _Rank =
  1000000000000
      + ROUND ( valueMain + ABS ( valueMin ), 0 ) --row value converted to a string that allows correct column sorting
VAR _SVG_Width = 400
VAR _ColorGrey = "#A6A6A6"
VAR _ColorRed = "#FF0000"
VAR _ColorGreen = "#8CB400"
VAR _Em = 16 * 4 / 3 --px
VAR _SVG_LeftPadding = 180
VAR _SVG_RightPadding = 180
VAR _SVG_ColumnWidth = _SVG_Width - _SVG_LeftPadding - _SVG_RightPadding
VAR _Scale =
  DIVIDE ( _SVG_ColumnWidth, valueScale )
VAR _FontWeight =
  --'normal' font for sales persons, 'bold' font for total row (average)
  IF (
      isTotalRow,
      "bold",
      "normal"
  ) //
//=================================================================================================
//SCALING: converting data values into X and Y positions on the SVG plot
//=================================================================================================
VAR _AxisYPosition =
  _SVG_LeftPadding
      + IF (
          valueMin >= 0,
          0,
          ROUND (
              DIVIDE ( ABS ( valueMin ), ABS ( valueMin ) + valueMax ) * _SVG_ColumnWidth,
              0
          )
      )
VAR _WidthValue =
  --bar width (numeric value)
  ROUND ( ABS ( valueMain ) * _Scale, 0 )
VAR _barColor =
  --green or red
  IF ( valueMain > 0, _ColorGreen, _ColorRed )
VAR _X =
  --x position of a bar
  SWITCH (
      TRUE (),
      valueMain >= 0, _AxisYPosition,
      valueMain < 0, _AxisYPosition - _WidthValue
  )
VAR _XText =
  --x position of a label
  SWITCH (
      TRUE (),
      valueMain >= 0, _AxisYPosition + _WidthValue,
      valueMain < 0, _X
  )
VAR _Anchor =
  --text anchor
  IF (
      isTotalRow,
      "middle",
      IF ( valueMain >= 0, "start", "end" )
  )
VAR _DX =
  --text offset along axis X
  SWITCH (
      TRUE (),
      valueMain >= 0, 5,
      valueMain < 0, -5
  ) //
//=================================================================================================    
//GENERATE SVG: concatenating parts of the SVG image
//=================================================================================================
VAR _SVG_Header = "data:image/svg+xml;utf8,"
VAR _SVG_Open = "<svg xmlns='http://www.w3.org/2000/svg' width='" & _SVG_Width & "' >"
VAR _SVG_Style = "<style>
    text{
      font-family: Inter, sans-serif;
      font-size: " & _Em & "px;
      font-weight: " & _FontWeight & ";
      dominant-baseline: central;
      text-anchor: " & _Anchor & ";
    }
</style>"
VAR _SVG_Close = "</svg>"
VAR _SVG_SortBy = "/*SortBy:" & _Rank & "*/" //VAR _SVG_Box = "<rect x='0%' y='0%' width='100%' height='100%' fill='#FFAAAA' />"
VAR _SVG_AxisY = "<line  x1='" & _AxisYPosition & "' x2='" & _AxisYPosition & "' y1='0%' y2='100%' stroke='" & _ColorGrey & "' stroke-width='" & _Em * 0.2 & "'></line >"
VAR _SVG_BarDeltaPY = "<rect x='" & _X & "' width='" & _WidthValue & "' y='" & _Em * 0.25 & "' height='" & _Em & "' fill='" & _barColor & "' stroke='" & _barColor & "' stroke-width='0.5'></rect>"
VAR _SVG_TextDeltaPYLabel = "<text x='" & _XText & "' dx='" & _DX & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_TextDeltaPYLabelTotal = "<text x='" & _AxisYPosition & "' dx='" & 0 & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_Total = _SVG_Header & _SVG_Open & _SVG_TextDeltaPYLabelTotal & _SVG_Style & _SVG_Close
VAR _SVG_Row = _SVG_Header & _SVG_Open & _SVG_SortBy & _SVG_SortBy & _SVG_AxisY & _SVG_BarDeltaPY & _SVG_TextDeltaPYLabel & _SVG_Style & _SVG_Close //VAR _SVG_SubRow = _SVG_Header & _SVG_Open & "<text x='" & _SVG_LeftPadding & "' dx='" & 0 & "' y='50%' font-weight='" & _FontWeight & "' text-anchor='end'>" & dataLabel & "</text>" & _SVG_Style & _SVG_Close
VAR _SVG =
  IF ( isTotalRow, _SVG_Total, _SVG_Row )
RETURN
  _SVG

EVALUATE
  {
      PowerofBI.IBCS.BarChart.AbsoluteVariance()
  }

This DAX function creates an SVG diverging bar chart that shows the variance between two values, including:

TermMeaning
valueMainActual variance (AC - PY)
Green barPositive variance (good result)
Red barNegative variance (bad result)
Vertical lineZero line (the starting point)

Read the Explanation

Parameter Explanation (Inputs)

ParameterMeaning
valueMainThe variance value (AC – PY)
dataLabelText to display (usually formatted variance like "+5%" or "-10%")
isTotalRowTRUE if this row is a total/summary row
valueMaxHighest variance in the table (used to scale bar size)
valueMinLowest variance in the table
valueScaleMax absolute value of AC & PY (for scaling)

MAIN LOGIC – What the Code Does

1. Calculate Bar Size and Position

DAX
_Scale = DIVIDE(_SVG_ColumnWidth, valueScale)
_WidthValue = ROUND(ABS(valueMain) * _Scale, 0)

Purpose: Convert variance value into pixels (so bigger variance = longer bar).


2. Decide Where the ZERO Line Is

DAX
_AxisYPosition = _SVG_LeftPadding + IF(
  valueMin >= 0,
  0,
  ROUND(DIVIDE(ABS(valueMin), ABS(valueMin) + valueMax) * _SVG_ColumnWidth, 0)
)

Purpose:

  • If all values are positive → zero is at the left

  • If there are negative values → find where the ZERO line should be placed


3. Decide Bar Color

DAX
_barColor = IF(valueMain > 0, _ColorGreen, _ColorRed)

✔ Positive = Green

❌ Negative = Red


4. Decide Where to Start Drawing the Bar

DAX
_X = SWITCH(
  TRUE(),
  valueMain >= 0, _AxisYPosition,
  valueMain < 0, _AxisYPosition - _WidthValue
)
  • If positive → start at zero and go RIGHT

  • If negative → start at zero and go LEFT


5. Place the Label

DAX
_XText = IF(valueMain >= 0, _AxisYPosition + _WidthValue, _X)

Labels go at the END of the bar.


6. Generate the SVG

DAX
_SVG_Row = _SVG_Header & _SVG_Open & _SVG_SortBy &
         _SVG_AxisY & _SVG_BarDeltaPY &
         _SVG_TextDeltaPYLabel & _SVG_Style & _SVG_Close

⚠️ Important Note

To guarantee consistent scaling across all chart tiers (particularly between the first and second tiers), the total sum of the left and right padding must be identical for every tier. This rule is critical because the padding total defines the available space for the axis scale.

The padding can be adjusted by simply replacing the values from the following lines:

DAX
VAR _SVG_LeftPadding =  5 --left side padding (before Y axis)
VAR _SVG_RightPadding = 355 --right side padding (space for data labels)

Example and Justification

  • Tier 1 (Base Chart): Often uses asymmetrical padding to fit labels or totals.

    • Padding: Left 5 + Right 355 = Total 360
  • Tier 2 (Absolute Variance Chart): Must match the total padding of Tier 1 while maintaining a symmetrical axis. Since a variance chart contains both negative and positive values, the zero line must be centered.

    • Required Total Padding: 360
    • Required Symmetry: The padding must be split evenly (360 / 2 = 180).
    • Resulting Padding: Left 180 and Right 180

By ensuring both tiers have a total padding of 360, the scales remain consistent. The symmetrical padding in Tier 2 ensures its axis remains centered around zero.

UDF #03: Relative Variance Chart (Third Tier)

DAX
DEFINE
  FUNCTION PowerofBI.IBCS.BarChart.RelativeVariance = /// Creates a bar chart that shows the relative (%) variance between two values
/// @param {scalar} valueMain
/// @param {string} dataLabel
/// @param {boolean} isTotalRow
/// @param {scalar} valueMax - Maximum value in the dataset for proper scaling
/// @returns SVG image
(
  // Actual value
  valueMain: SCALAR VAL,
  // Data label
  dataLabel: STRING VAL,  
  // is it a total row: TRUE() or FALSE(); ; use NOT ( ISINSCOPE () )
  isTotalRow: BOOLEAN VAL,
  // Maximum value from all rows (for scaling)
  valueMax: SCALAR VAL
) 
=> 
//=================================================================================================
//Project: an UDF for generating IBCS-styled charts (as SVG images)
//  The visualization design follows IBCS guidelines https://www.ibcs.com/ibcs-standards-1-2/
//  Although it is not produced, not certified, not authorized, not endorsed by IBCS® https://www.ibcs.com/
//  The charts can be embedded into Table, Matrix, and New Card core Power BI visuals
//=================================================================================================
//Author: Andrzej Leszkiewicz, an IBCS® Certified Analyst
//  For more information, visit https://powerofbi.org/
//  Connect on LinkedIn at https://www.linkedin.com/in/avatorl/
//  Contact via email at andrzej@powerofbi.org
//=================================================================================================
//License: MIT Expat License https://en.wikipedia.org/wiki/MIT_License (free)
//=================================================================================================
//Returns an SVG image code with a pin chart
//Relative variance, for example ΔPY% = ((AC+FC)-PY)/PY in %
//red pins - negative values, green pins - positive values
//grey axis Y (PY)
//
//======================================================================================
//CONFIG - ADJUST THESE VALUES TO CONTROL OUTLIER TRUNCATION
//======================================================================================
VAR _outlierCap = 2 
  --Maximum value to display in DECIMAL format (e.g., 1.5 = 150%, 2.0 = 200%)
  --Values larger than this will be truncated and marked with a triangle
  --ADJUST THIS VALUE based on your data: 
  --  For values like 43.7 (4,370%), try: 1.5 to 3.0
  --  1.0 = 100%, 1.5 = 150%, 2.0 = 200%, 2.5 = 250%, 3.0 = 300%
//======================================================================================
VAR _minDelta = 0
VAR _Rank =
  1000000000000
      + ROUND ( valueMain + 100, 0 ) --row value converted to a string that allows correct column sorting
VAR _IsOutlier = 
  NOT( ISBLANK( valueMain ) ) && ABS( valueMain ) > _outlierCap --Check if value exceeds the cap
      
VAR _ValueForDisplay =
  --Truncate values that exceed the outlier cap
  IF( 
      ABS( valueMain ) > _outlierCap,
      _outlierCap * IF( valueMain < 0, -1, 1 ),
      valueMain
  )
  
VAR _ColorGrey = "#A6A6A6"
VAR _ColorRed = "#FF0000"
VAR _ColorGreen = "#8CB400"

//======================================================================================
// TRIANGLE MARKER OPTIONS - Choose one by uncommenting:
//======================================================================================
VAR _OutlinerTriangle = UNICHAR(9654)   -- ► Black Right-Pointing Pointer (recommended)
// VAR _OutlinerTriangle = UNICHAR(9656)   -- ▸ Black Right-Pointing Small Triangle
// VAR _OutlinerTriangle = UNICHAR(9658)   -- ► Black Right-Pointing Triangle
// VAR _OutlinerTriangle = UNICHAR(8227)   --  Triangular Bullet
// VAR _OutlinerTriangle = UNICHAR(9205)   -- ⏵ Medium Right-Pointing Triangle
// VAR _OutlinerTriangle = "►"             -- Direct character (may work better in some fonts)
//======================================================================================

VAR _Em = 16 * 4 / 3 --px
VAR _FontWeight =
  --'normal' font for sales persons, 'bold' font for total row (average)
  IF (
      isTotalRow,
      "bold",
      "normal"
  )
//Use the smaller of: the outlier cap or the max value from dataset
VAR _maxValue = MIN( _outlierCap, ABS( valueMax ) )
VAR _SVG_Width = 400
VAR _SVG_Height = _Em * 1.62
VAR _SVG_LeftPadding = 150 
VAR _SVG_RightPadding = 150
VAR _SVG_ColumnWidth = _SVG_Width - _SVG_LeftPadding - _SVG_RightPadding 
//SCALING
VAR _AxisYPosition =
  _SVG_LeftPadding
      + IF (
          _minDelta >= 0,
          0,
          ROUND ( DIVIDE ( ABS ( _minDelta ), _maxValue ) * _SVG_ColumnWidth, 0 )
      )
      
VAR _WidthValue =
  --bar width (numeric value) - use the display value (capped)
  ROUND (
      DIVIDE ( ABS ( _ValueForDisplay ), _maxValue ) * _SVG_ColumnWidth,
      0
  )
  
VAR _barColor =
  --green or red
  IF ( valueMain > 0, _ColorGreen, _ColorRed )
  
VAR _X =
  --x position of a bar
  SWITCH (
      TRUE (),
      _ValueForDisplay >= 0, _AxisYPosition,
      _ValueForDisplay < 0, _AxisYPosition - _WidthValue
  )
  
VAR _XPinhead =
  --x position of a bar (pinhead)
  SWITCH (
      TRUE (),
      _ValueForDisplay >= 0,
          _AxisYPosition + _WidthValue - _Em * 0.4,
      _ValueForDisplay < 0,
          _AxisYPosition - _WidthValue - _Em * 0.3
  )
  
VAR _Anchor =
  --text anchor
  IF (
      isTotalRow,
      "middle",
      IF ( _ValueForDisplay >= 0, "start", "end" )
  )
  
VAR _DX =
  --text offset along axis X
  SWITCH (
      TRUE (),
      _ValueForDisplay >= 0,
          IF ( _IsOutlier, _Em * 0.3, _Em * 0.7 ) + 5,
      _ValueForDisplay < 0, - 5
  ) 
VAR _LabelLength = 
  --Approximate label width in pixels (average character width * number of characters)
  LEN(dataLabel) * _Em * 0.5
VAR _XTriangle = 
  --x position for outlier triangle marker (to the right of data label)
  IF(
      _ValueForDisplay >= 0,
      _XPinhead + _DX + _LabelLength + 15,
      _XPinhead + _DX - 15
  )
  
//=================================================================================================    
//GENERATE SVG: concatenating parts of the SVG image
//=================================================================================================
VAR _SVG_Header = "data:image/svg+xml;utf8,"
VAR _SVG_Open = "<svg xmlns='http://www.w3.org/2000/svg' width='" & _SVG_Width & "' height='" & _SVG_Height & "' >"
VAR _SVG_Style = "<style>
    text{
      font-family: Inter, sans-serif;
      font-size: " & _Em & "px;
      font-weight: " & _FontWeight & ";
      dominant-baseline: central;
      text-anchor: " & _Anchor & ";
    }
    #markoutlier{
      dominant-baseline: middle;
      font-size: " & _Em * 1 & "px;
    }
</style>"
VAR _SVG_Close = "</svg>"
VAR _SVG_SortBy = "/*SortBy:" & _Rank & "*/"
VAR _SVG_AxisY = "<line id='line-axisy' x1='" & _AxisYPosition & "' x2='" & _AxisYPosition & "' y1='0%' y2='100%' stroke='" & _ColorGrey & "' stroke-width='" & _Em * 0.2 & "'></line >"
VAR _SVG_BarDeltaPY = "<rect id='rect-variancepin' x='" & _X & "' width='" & _WidthValue & "' y='" & _Em * 0.6 & "' height='" & _Em * 0.3 & "' fill='" & _barColor & "' stroke='" & _barColor & "' stroke-width='0.5'></rect>"
VAR _SVG_PinHead = "<rect id='rect-pinhead' x='" & _XPinhead & "' width='" & _Em * 0.7 & "' y='" & _Em * 0.3 & "' height='" & _Em * 0.9 & "' fill='#000000'></rect>"
VAR _SVG_TextDeltaPYLabel = "<text id='text-label' text-anchor='" & _Anchor & "' x='" & _XPinhead & "' dx='" & _DX & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_Outliner =
  "<text id='markoutlier' text-anchor='start' x='" & _XTriangle & "' fill='" & _barColor & "' y='50%'>" & _OutlinerTriangle & "</text>"
VAR _SVG_PinHeadX =
  IF ( _IsOutlier, "", _SVG_PinHead )
  
VAR _SVG_OutlierMarker =
  IF ( _IsOutlier, _SVG_Outliner, "" )
  
VAR _SVG_Debug = "<!--IsOutlier:" & _IsOutlier & " Value:" & valueMain & " Cap:" & _outlierCap & "-->"
VAR _SVG_TextDeltaPYLabelTotal = "<text id='text-labeltotal' x='" & _AxisYPosition & "' y='50%' >" & dataLabel & "</text>"
VAR _SVG_Total = _SVG_Header & _SVG_Open & _SVG_TextDeltaPYLabelTotal & _SVG_Style & _SVG_Close
VAR _SVG_Row = _SVG_Header & _SVG_Open & _SVG_Debug & _SVG_SortBy & _SVG_AxisY & _SVG_PinHeadX & _SVG_BarDeltaPY & _SVG_TextDeltaPYLabel & _SVG_OutlierMarker & _SVG_Style & _SVG_Close
VAR _SVG =
  IF ( isTotalRow, _SVG_Total, _SVG_Row )
RETURN
  _SVG

EVALUATE
  {
      PowerofBI.IBCS.BarChart.RelativeVariance()
  }

This User-Defined Function (UDF) in DAX generates an SVG image — a small bar chart (pin chart) inside a table or matrix visual in Power BI. It follows IBCS standards to show relative variance (%) between scenarios.

Read the Explanation

Function Purpose

It creates a small bar chart with:

Visual ElementMeaning
Gray vertical linePY baseline (previous year)
Green barPositive variance (Actual > PY)
Red barNegative variance (Actual < PY)
Black rectanglePinhead (end of bar)
Text% value label like +23%
Triangle markerIf variance is too big → show triangle to indicate “Outlier”

Function Parameters Explained

The function takes 4 inputs:

ParameterMeaning
valueMainThe ΔPY% value (the variance %)
dataLabelText shown next to it (e.g., “+23%”)
isTotalRowTRUE if this is a Total/Average row
valueMaxThe highest variance value in the dataset → used for scaling

Key Logic

1. Outlier Detection

DAX
VAR _outlierCap = 2   // → Anything above 200% will be capped
VAR _IsOutlier = ABS(valueMain) > _outlierCap

If the variance is too big (e.g., +430%), it will not draw the full bar. Instead, it cuts it and shows a triangle ( ► ) to warn users.


2. Capping the Value

DAX
VAR _ValueForDisplay =
  IF( ABS(valueMain) > _outlierCap,
      _outlierCap * IF(valueMain < 0, -1, 1),
      valueMain
  )
  • If value is within limit → draw normally

  • If too large → cut at the limit but keep the direction (positive/negative)


3. Choosing Bar Color

DAX
VAR _barColor = IF (valueMain > 0, _ColorGreen, _ColorRed)
  • Positive → Green

  • Negative → Red


4. SVG Creation

At the end, all pieces are glued together into an SVG code:

DAX
VAR _SVG =
  IF ( isTotalRow, _SVG_Total, _SVG_Row )
RETURN _SVG
  • If it’s a Total row → show only label (no bar).

  • If it’s a normal row → show full bar + label + triangle (if needed).


Final Output

It returns a string starting with:

DAX
data:image/svg+xml;utf8,<svg ...>
  • Power BI interprets this string as an image.

  • When used in a Table/Matrix, it becomes a mini visual bar/chart.

Step 3: Generating the SVG Bar Charts

Once the necessary User-Defined Functions (UDFs) are defined, the next step is to create the DAX measure required to generate the SVG image output. The DAX measure for the first-tier bar chart, which displays the grouped bar chart, is defined as follows:

Measure #01: Grouped Bar Chart (First Tier)

DAX
_01 Grouped Bar (AC & PY) = 
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]
VAR _HasData = NOT(ISBLANK(_AC)) || NOT(ISBLANK(_PY))
RETURN
  IF(
      _HasData,
      PowerofBI.IBCS.BarChart.AbsoluteValues (
          _AC,
          0,  -- or your FC value
          _PY,
          "grey",  -- or "outlined"
          FORMAT(_AC, "#,##0.0"),  -- your label format
          NOT ( ISINSCOPE ( DimLocation[State] ) ),
          MAX(
              MAXX(ALLSELECTED(DimLocation), [_01 Total Net Sales]),
              MAXX(ALLSELECTED(DimLocation), [_02 PY Net Sales])
          )
      ),
      BLANK()
  )

This measure generates an IBCS-style bar chart (using SVG) to compare PY (Previous Year) and AC (Actual / Current Year). It uses the PowerofBI.IBCS.BarChart.AbsoluteValues function to draw two bars: one for AC and one for PY.

Read the Explanation

Step-by-Step Explanation

1. Get the values

DAX
VAR _AC = [_01 Total Net Sales]
VAR _PY = [_02 PY Net Sales]

We get the actual (current) net sales and previous year net sales.


2. Check if at least one value exists

DAX
VAR _HasData = NOT(ISBLANK(_AC)) || NOT(ISBLANK(_PY))
  • If both are blank → don’t show the bar chart

  • If at least one exists → show the chart


3. If data exists → generate the bar chart

DAX
IF( _HasData,
  PowerofBI.IBCS.BarChart.AbsoluteValues (...),
  BLANK()
)
  • If data exists → call the custom IBCS bar chart function

  • If no data → return BLANK (Power BI will show nothing)


4. Parameters Passed into the Chart Function

The chart function requires several inputs, let’s explain each:

DAX
PowerofBI.IBCS.BarChart.AbsoluteValues (
  _AC,                           -- Actual Net Sales
  0,                            -- FC (Forecast) → not used here
  _PY,                          -- PY Net Sales
  "grey",                       -- Chart style → grey bars (IBCS style)
  FORMAT(_AC, "#,##0.0"),       -- Label format (will display AC value)
  NOT(ISINSCOPE(DimLocation[State])), -- TRUE for total row
  MAX(                          -- Find the maximum value for scaling the bars
      MAXX(ALLSELECTED(DimLocation), [_01 Total Net Sales]),
      MAXX(ALLSELECTED(DimLocation), [_02 PY Net Sales])
  )
)

The following DAX measure defines the absolute variance between AC and PY values displayed in the second-tier bar chart.

Measure #02: Absolute Variance Chart (Second Tier)

DAX
_02 Absolute Variance Bar (△PY) = 
VAR _HasVariance = NOT(ISBLANK([_03 △PY Net Sales]))
VAR _HasAnyPYData = 
  CALCULATE(
      COUNTROWS(
          FILTER(
              ALLSELECTED(DimLocation[State]),
              NOT(ISBLANK([_02 PY Net Sales]))
          )
      )
  ) > 0
RETURN
  IF(
      _HasVariance && _HasAnyPYData,  -- Show only if variance exists AND at least one state has PY data
      PowerofBI.IBCS.BarChart.AbsoluteVariance (
          [_03 △PY Net Sales],
          FORMAT ( [_03 △PY Net Sales], "+0.0;-0.0;0" ),
          NOT ( ISINSCOPE ( DimLocation[State] ) ),
          MAXX ( ALLSELECTED ( DimLocation ), [_03 △PY Net Sales] ),
          MINX ( ALLSELECTED ( DimLocation ), [_03 △PY Net Sales] ),
          MAX (
              MAXX ( ALLSELECTED ( DimLocation ), [_01 Total Net Sales] ),
              MAXX ( ALLSELECTED ( DimLocation ), [_02 PY Net Sales] )
          )    
      ),
      BLANK()
  )

This DAX creates a visual bar chart (using SVG) that shows the absolute variance (difference in sales) between AC and PY (also called ΔPY (Delta PY)). It only shows the visual if it makes sense — meaning:

  1. There must be a variance value

  2. There must be at least one state with PY data

Otherwise, Power BI shows blank (to avoid misleading visuals).

Read the Explanation

Step-by-Step Breakdown

1. Check if there is any variance

DAX
VAR _HasVariance = NOT(ISBLANK([_03 △PY Net Sales]))
  • If _03 △PY Net Sales is blank → nothing to show

  • If it has a value → there is a difference between AC and PY


2. Check if there is ANY PY data in the dataset

DAX
VAR _HasAnyPYData =
  CALCULATE(
      COUNTROWS(
          FILTER(
              ALLSELECTED(DimLocation[State]),
              NOT(ISBLANK([_02 PY Net Sales]))
          )
      )
  ) > 0

What it does:

  • Looks at all selected states

  • Checks how many states have a PY value (not blank)

  • If at least 1 state has PY data → returns TRUE


3. Only show chart if BOTH conditions are TRUE

DAX
IF(
  _HasVariance && _HasAnyPYData,
  PowerofBI.IBCS.BarChart.AbsoluteVariance (...),
  BLANK()
)

Final decision:

ConditionShow Chart?
No varianceNo
No PY dataNo
Both existYes

4. If TRUE → Draw Absolute Variance Bar Chart

DAX
PowerofBI.IBCS.BarChart.AbsoluteVariance (
  [_03 △PY Net Sales],                            -- actual variance value
  FORMAT([_03 △PY Net Sales], "+0.0;-0.0;0"),     -- label format (+5.2 / -3.1)
  NOT(ISINSCOPE(DimLocation[State])),             -- total row check
  MAXX(ALLSELECTED(DimLocation), [_03 △PY Net Sales]), -- highest variance
  MINX(ALLSELECTED(DimLocation), [_03 △PY Net Sales]), -- lowest variance
  MAX(                                         -- scale limit
      MAXX(ALLSELECTED(DimLocation), [_01 Total Net Sales]),
      MAXX(ALLSELECTED(DimLocation), [_02 PY Net Sales])
  )
)

What These Parameters Do:

ParameterPurpose
Variance valueDraws bar length & direction
Label formatShows + or – number
ISINSCOPEHandles total row formatting
MAX varianceScaling (positive side)
MIN varianceScaling (negative side)
Largest AC/PY valueEnsures chart size is consistent

The following DAX measure defines the relative variance between AC and PY values displayed in the third-tier bar chart.

Measure #03: Relative Variance Chart (Third Tier)

DAX
_03 Relative Variance Bar (△PY%) = 
VAR _HasVariance = NOT(ISBLANK([_04 △PY% Net Sales]))
VAR _HasAnyPYData = 
  CALCULATE(
      COUNTROWS(
          FILTER(
              ALLSELECTED(DimLocation[State]),
              NOT(ISBLANK([_02 PY Net Sales]))
          )
      )
  ) > 0
VAR _MaxValue =
  MAXX (
      ALLSELECTED ( DimLocation[State] ),
      ABS ( [_04 △PY% Net Sales] )
  )
RETURN
  IF(
      _HasVariance && _HasAnyPYData,  -- Show only if variance exists AND at least one state has PY data
      PowerofBI.IBCS.BarChart.RelativeVariance (
          [_04 △PY% Net Sales],
          IF (
              ABS ( [_04 △PY% Net Sales_Formatted] ) >= 1000,
              FORMAT ( [_04 △PY% Net Sales_Formatted], "+# ##0.0;-# ##0.0;0" ),
              FORMAT ( [_04 △PY% Net Sales_Formatted], "+0.0;-0.0;0" )
          ),
          NOT ( ISINSCOPE ( DimLocation[State] ) ),
          _MaxValue
      ),
      BLANK()
  )

This measure creates a relative (%) variance chart using the IBCS pin-style visual. It only shows the visual when it makes sense:

  1. There must be a real variance.

  2. There must be PY data for at least one state.

Otherwise → Power BI shows blank to avoid misleading visuals.

Read the Explanation

MAIN PURPOSE

To show how much we grew/shrank compared to last year, in percentage (%).

Example:

AC (Current Year)PY (Previous Year)Result
120100+20%
70100-30%
(Blank)100-100%
200(Blank)+100%

Step-by-Step Breakdown

1. Check if there is any variance

DAX
VAR _HasVariance = NOT(ISBLANK([_04 △PY% Net Sales]))

If _04 △PY% Net Sales has no value → no need to show anything


2. Check if ANY PY data exists

DAX
VAR _HasAnyPYData =
  CALCULATE(
      COUNTROWS(
          FILTER(
              ALLSELECTED(DimLocation[State]),
              NOT(ISBLANK([_02 PY Net Sales]))
          )
      )
  ) > 0

Why this is important? We cannot calculate % variance if PY values are all missing. So this checks: Is there at least 1 state with PY data?


3. Get the largest value (for scaling the visual)

DAX
VAR _MaxValue =
  MAXX (
      ALLSELECTED ( DimLocation[State] ),
      ABS ( [_04 △PY% Net Sales] )
  )

Used to scale the bar length properly inside the SVG visual. Example: If the highest variance is +213%, the visual will use that as its scaling limit.


4. Final Output – Draw the chart OR show blank

DAX
RETURN
  IF(
      _HasVariance && _HasAnyPYData,
      PowerofBI.IBCS.BarChart.RelativeVariance (
          [_04 △PY% Net Sales],       -- Main numeric value
  1. If both conditions are TRUE → generate the chart

  2. If FALSE → return BLANK()


5. Handle Label Formatting (Smart Formatting)

DAX
IF (
  ABS ( [_04 △PY% Net Sales_Formatted] ) >= 1000,
  FORMAT ( [_04 △PY% Net Sales_Formatted], "+# ##0.0;-# ##0.0;0" ),
  FORMAT ( [_04 △PY% Net Sales_Formatted], "+0.0;-0.0;0" )
)

What this does:

Variance ValueFormatting Used
Less than 1,000%+5.6% / 3.2%
Greater than 1,000%+1 234.5% (uses space for thousands, following the IBCS principle)

6. Detect Total Row

DAX
NOT ( ISINSCOPE ( DimLocation[State] ) ),

If it’s a total row, the visual uses bold font & centered label (IBCS rule).


7. Pass the Max Value (for scaling)

DAX
_MaxValue

Used to normalize all bars so they are visually comparable.

⚠️ IMPORTANT NOTES

In the relative variance chart (tier 3), thinner bars are used to emphasize that the value is a relative measure, not an absolute volume (for example, 10% is 10% whether the base is small or large). The actual comparison period (AC) is denoted by a pinhead with a solid dark fill, and the comparison basis (PY) by the underlying gray axis line. To enable practical visualization for values that can drastically vary up to 4,000% in this specific scenario, all relative variance pins are capped at 200%.

Following IBCS recommendations, bars that exceed the cap are visually truncated; the pinhead is removed, and a triangle marker is placed next to the data label, pointing in the correct direction to signify that the value continues beyond the displayed bar.

After defining all the DAX measures:

  1. Change Data Category: For all defined DAX measures, change the Data Category to "Image URL" to render the SVG files.

  2. Configure Matrix Visual: Generate a matrix visual.

  • Place 'DimLocation'[Region] and 'DimLocation'[State] in the Rows field.
  • Place these three measures in the Values field.
  1. Ensure Correct Sizing: To display the SVGs properly, match the image size setting within the matrix visual to the dimensions defined in the UDFs.

Matrix Visual Configuration

Reference

Multi-tier bar charts • IBCS - International Business Communication Standards

View all articles