Deneb/Vega-Lite can be used to create inset charts in Power BI and improve the visualization of differences in datasets both with similar values and outlier values.
Nick Desbarats recently made several posts on inset charts; here’s a couple of links:
100 meters:
Outliers:
These piqued my interest to see if I could leverage Deneb and Vega-Lite to create examples of similar and outlier inset charts.
The examples herein present normal (full range) and inset (truncated range) bar charts for both similar and outlier data.
The Deneb/Vega-Lite code is quite similar for both, with only a few differences. The similar data example illustrates a number of specific Deneb/Vega-Lite features, including:
0 - General:
- a “title” block with multi-line subtitle (array)
- a “transform” block with:
- a “window/rank” transform to order the data (100m time: time and date [ascending])
- a “calculate” transform to determine the composite Y axis label (rank:name)
- 2x “joinaggretate” transforms to determine the maximum and minimum data values
- 2x “calculate” transforms to determine the inset chart range (100m time: 0.05 seconds below minimum and above maximum times)
- a “params” block with:
- a bar colour parameter (to reduce duplication/ensure consistency of bar colours)
- 2x direct data references to get single values for the inset chart range parameters
- a “hconcat” block to horizontally concatenate the normal (full range) and inset (truncated range) bar charts
1 - Normal Chart (full range):
- a “title” block with subtitle
- a shared “encoding” block to ensure the same X and Y are used for all layered marks
- an expression for the Y axis label to extract the “name” only from the composite label
- a “layer” block with:
- a “bar” mark for the normal bar with 80% opacity
- a “text” mark for the normal data label using built-in Vega-Lite SI formatting
2 - Inset Chart (truncated range):
- a “title” block with subtitle
- a shared “encoding” block to ensure the same X and Y are used for all layered marks
- no Y axis display (as already included in the normal chart)
- a “scale/domain” block to use the inset chart range parameters
- a “layer” block with:
- a “rect” mark of full X (width) and Y (height) in light grey to provide a subtle background colour difference from the normal chart
- a “bar” mark for the inset bar with:
- reduced band height to provide a subtle difference from the normal chart
- reduced opacity (60%) to provide a subtle difference from the normal chart
- a “clip/true” key:value pair to restrict the display to the specified range
- a “bar” mark for the gradient overlay with:
- a “filter” transform to restrict the dataset to the desired records (100m time: all [unnecessary but included for consistency with outlier code])
- a “color” block with linear gradient to apply conditional colour to the truncated end (100m time: white-to-blue at start)
- reduced band height to provide a subtle difference from the normal chart
- a “clip/true” key:value pair to restrict the display to the specified range
- an “encoding” block with X and X2 values to restrict the gradient to the desired range (starting 0.05)
- a “text” mark for the inset data label using built-in Vega-Lite SI formatting
The outlier data example uses Deneb/Vega-Lite code with only small differences from the similar example (as described above), including:
0 - General:
- a “transform” block with:
- a “window/rank” transform to order the data (country: area [descending])
- 2x “calculate” transforms to determine the normal chart threshold area and if the current record is below the normal chart threshold area (for subsequent use in data label alignment/position/colour)
- “calculate” and “joinaggregate” transforms to determine the maximum area of the non-outlier countries
- 2x “calculate” transforms to determine the inset chart threshold area and if the current record is below the inset chart threshold area (for subsequent use in data label alignment/position/colour)
- a “params” block with:
- a screen widget to permit user interaction for selection of the desired number of outliers
- a direct data reference to determine the area of the country immediately “below” the selected number of outliers (i.e., the non-outlier area)
- an expression to round up the non-outlier area to the nearest 5,000 for the upper end of the inset chart range
1 - Normal Chart (full range):
- in the “text” mark for the data label, use the “below normal threshold area” value (from above) to determine the alignment/position/colour
2 - Inset Chart (truncated range):
-
a shared “encoding” block to ensure the same X and Y are used for all layered marks
- no Y axis display (as already included in the normal chart)
- a “scale/domain” block to use a custom range for the inset chart (0 to 50,000 above the rounded-up non-outlier area)
-
a “layer” block with:
- a “bar” mark for the gradient overlay with:
- a “filter” transform to restrict the dataset to the desired records (country area: rank <= selected [in screen widget] number of outliers)
- a “color” block with linear gradient to apply conditional colour to the truncated end (country area: green-to-white at end)
- a “text” mark for the data label with:
- a “calculate” transform to determine if the current record rank is above the selected [in screen widget] number of outliers
- conditional alignment/position/colour using the above calculation
- a “bar” mark for the gradient overlay with:
Deneb/Vega-Lite JSON Code for the similar data:
{
"title": {
"anchor": "start",
"align": "left",
"offset": 20,
"text": "Power BI Inset Charts using Deneb",
"font": "Segoe UI",
"fontSize": 32,
"fontWeight": "bold",
"fontStyle": "normal",
"subtitle": [
"Similar Data - 100 Meter Time by Athlete (selected athletes only)",
"Data Source: Wikipedia, 2025-05-29",
"URL: https://en.wikipedia.org/wiki/100_metres"
],
"subtitleFont": "Segoe UI",
"subtitleFontSize": 18,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
"data": {
"name": "dataset"
},
"transform": [
{
"window": [
{
"op": "rank",
"as": "_performance_rank"
}
],
"sort": [
{
"field": "Time (s)",
"order": "ascending"
},
{
"field": "Date",
"order": "ascending"
}
]
},
{
"calculate": "pad( datum['_performance_rank'], 2, '0', 'left' ) + '-' + datum['Athlete']",
"as": "_performance_rank_athlete_name_label"
},
{
"joinaggregate": [
{
"op": "max",
"field": "Time (s)",
"as": "_max_time"
},
{
"op": "min",
"field": "Time (s)",
"as": "_min_time"
}
]
},
// NOTE: subtract 0.05 to enforce roundDown
{
"calculate": "toNumber( format( datum['_min_time'] - 0.05, '.2r' ) )",
"as": "_inset_chart_min_value"
},
// NOTE: add 0.05 to enforce roundUp
{
"calculate": "toNumber( format( datum['_max_time'] + 0.05, '.2r' ) )",
"as": "_inset_chart_max_value"
}
],
"params": [
{
"name": "_bar_colour",
"value": "#0F4C81"
},
{
"name": "_inset_chart_lower_value",
"expr": "data('data_0')[0]['_inset_chart_min_value']"
},
{
"name": "_inset_chart_upper_value",
"expr": "data('data_0')[0]['_inset_chart_max_value']"
}
],
"spacing": 10,
"hconcat": [
{
"name": "NORMAL_CHART",
"title": {
"text": "Normal Chart",
"font": "Segoe UI",
"fontSize": 14,
"fontStyle": "italic",
"fontWeight": "bold",
"subtitle": "Full Range (difficult to see differences)",
"subtitleFont": "Segoe UI",
"subtitleFontSize": 12,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
// TO DO: investigate how to use an expression of width here
"width": 500,
"height": 500,
"encoding": {
"y": {
"field": "_performance_rank_athlete_name_label",
"type": "nominal",
"sort": "x",
"axis": {
"title": null,
"offset": 4,
"labelExpr": "slice( datum.value, 3, 100 )",
"labelFont": "Segoe UI",
"labelFontSize": 16,
"domain": false,
"ticks": false
}
},
"x": {
"field": "Time (s)",
"type": "quantitative",
"axis": {
"title": null,
"labelFlush": false
}
}
},
"layer": [
{
"name": "NORMAL_BAR",
"mark": {
"type": "bar",
"color": {
"expr": "_bar_colour"
},
"opacity": 0.8
}
},
{
"name": "NORMAL_DATA_LABEL",
"mark": {
"type": "text",
"align": "left",
"xOffset": 4,
"font": "Segoe UI",
"fontSize": 12
},
"encoding": {
"text": {
"field": "Time (s)",
"type": "quantitative",
"format": "0.3s"
}
}
}
]
},
{
"name": "INSET_CHART",
"title": {
"text": "Inset Chart",
"font": "Segoe UI",
"fontSize": 14,
"fontStyle": "italic",
"fontWeight": "bold",
"subtitle": "Truncated Range (start omitted for all; easier to see differences)",
"subtitleFont": "Segoe UI",
"subtitleFontSize": 12,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
// TO DO: investigate how to use an expression of width here
"width": 450,
"height": 500,
"encoding": {
"y": {
"field": "_performance_rank_athlete_name_label",
"type": "nominal",
"sort": "x",
"axis": null
},
"x": {
"field": "Time (s)",
"type": "quantitative",
"axis": {
"title": null,
"labelFlush": false,
"gridColor": "#E3E3E3",
"gridOpacity": 1.0
},
"scale": {
"domain": [
{
"expr": "_inset_chart_lower_value"
},
{
"expr": "_inset_chart_upper_value"
}
]
}
}
},
"layer": [
{
"name": "INSET_BACKGROUND",
"mark": {
"type": "rect",
"color": "#E3E3E3",
"opacity": 0.25
},
"encoding": {
"x": {
"datum": 9.5
},
"x2": {
"datum": 9.9
}
}
},
{
"name": "INSET_BAR",
"mark": {
"type": "bar",
"color": {
"expr": "_bar_colour"
},
"height": {
"band": 0.8
},
"opacity": 0.6,
// NOTE: restrict the domain to the specified range
"clip": true
}
},
{
"name": "INSET_GRADIENT_BAR",
"transform": [
// NOTE: filter included for consistency with outlier inset chart only (all records returned)
{
"filter": "datum['_performance_rank'] <= 100"
}
],
"mark": {
"type": "bar",
"color": {
"x1": 0,
"y1": 0,
"x2": 1,
"y2": 0,
"gradient": "linear",
"stops": [
{
"offset": 0,
"color": "white"
},
{
"offset": 1,
// hard-coded colour: Vega-Lite expects string, not parameter
"color": "#0F4C8100"
}
]
},
"height": {
"band": 0.8
},
// restrict the domain to the specified range
"clip": true
},
"encoding": {
"x": {
"datum": {
"expr": "_inset_chart_lower_value"
}
},
"x2": {
"datum": {
"expr": "_inset_chart_lower_value + 0.05"
}
}
}
},
{
"name": "INSET_DATA_LABEL",
"mark": {
"type": "text",
"align": "left",
"xOffset": 4,
"font": "Segoe UI",
"fontSize": 12
},
"encoding": {
"text": {
"field": "Time (s)",
"type": "quantitative",
"format": "0.3s"
}
}
}
]
}
]
}
Deneb/Vega-Lite JSON Code for the outlier data:
{
"title": {
"anchor": "start",
"align": "left",
"offset": 20,
"text": "Power BI Inset Charts using Deneb",
"font": "Segoe UI",
"fontSize": 32,
"fontWeight": "bold",
"fontStyle": "normal",
"subtitle": [
"Outlier Data - Countries by Area (selected countries only)",
"Data Source: Wikipedia, 2025-05-26",
"URL: https://en.wikipedia.org/wiki/List_of_countries_and_dependencies_by_area"
],
"subtitleFont": "Segoe UI",
"subtitleFontSize": 18,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
"data": {
"name": "dataset"
},
"transform": [
{
"window": [
{
"op": "rank",
"as": "_country_area_rank"
}
],
"sort": [
{
"field": "Area (km2)",
"order": "descending"
}
]
},
{
"calculate": "pad( datum['_country_area_rank'], 2, '0', 'left' ) + '-' + datum['Country']",
"as": "_country_area_rank_country_name_label"
},
{
"joinaggregate": [
{
"op": "max",
"field": "Area (km2)",
"as": "_max_country_area"
},
{
"op": "min",
"field": "Area (km2)",
"as": "_min_country_area"
}
]
},
{
"calculate": "datum['_max_country_area'] * 0.8",
"as": "_normal_threshold_area"
},
{
"calculate": "datum['Area (km2)'] < datum['_normal_threshold_area']",
"as": "_below_normal_threshold_area"
},
{
"calculate": "if( datum['_country_area_rank'] == ( _outliers + 1 ), datum['Area (km2)'], null )",
"as": "_inset_non_outlier_country_area"
},
{
"joinaggregate": [
{
"op": "max",
"field": "_inset_non_outlier_country_area",
"as": "_max_inset_non_outlier_country_area"
}
]
},
{
"calculate": "if( datum['_country_area_rank'] <= _outliers, datum['_max_inset_non_outlier_country_area'] + 5000, datum['Area (km2)'] )",
"as": "_inset_chart_country_area"
},
{
"calculate": "datum['_max_inset_non_outlier_country_area'] * 0.8",
"as": "_inset_threshold_area"
},
{
"calculate": "datum['Area (km2)'] < datum['_inset_threshold_area']",
"as": "_below_inset_threshold_area"
}
],
"params": [
{
"name": "_bar_colour",
"value": "#013220"
},
{
"name": "_inset_chart_upper_value",
"expr": "data('data_0')[_outliers]['_inset_chart_country_area']"
},
// NOTE: add 5000 to enforce roundUp
{
"name": "_inset_chart_upper_value2",
"expr": "toNumber( format( _inset_chart_upper_value + 5000, '.2r' ) )"
},
{
"name": "_outliers",
"value": 2,
"bind": {
"input": "range",
"min": 1,
"max": 5,
"step": 1,
"name": "Outliers: "
}
}
],
"spacing": 10,
"hconcat": [
{
"name": "NORMAL_CHART",
"title": {
"text": "Normal Chart",
"font": "Segoe UI",
"fontSize": 14,
"fontStyle": "italic",
"fontWeight": "bold",
"subtitle": "Full Range (difficult to see differences between non-outlier values)",
"subtitleFont": "Segoe UI",
"subtitleFontSize": 12,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
// TO DO: investigate how to use an expression of width here
"width": 500,
"height": 470,
"encoding": {
"y": {
"field": "_country_area_rank_country_name_label",
"type": "nominal",
"sort": "-x",
"axis": {
"title": null,
"offset": 4,
"labelExpr": "slice( datum.value, 3, 100 )",
"labelFont": "Segoe UI",
"labelFontSize": 16,
"domain": false,
"ticks": false
}
},
"x": {
"field": "Area (km2)",
"type": "quantitative",
"axis": {
"title": null,
"labelFlush": false,
"formatType": "pbiFormat",
"format": "0,,.# M"
}
}
},
"layer": [
{
"name": "NORMAL_BAR",
"mark": {
"type": "bar",
"color": {
"expr": "_bar_colour"
},
"opacity": 0.8
}
},
{
"name": "NORMAL_DATA_LABEL",
"mark": {
"type": "text",
"align": {
"expr": "if( datum['_below_normal_threshold_area'] == true, 'left', 'right' )"
},
"xOffset": {
"expr": "if( datum['_below_normal_threshold_area'] == true, 4, -4 )"
},
"font": "Segoe UI",
"color": {
"expr": "if( datum['_below_normal_threshold_area'] == true, 'black', 'white' )"
}
},
"encoding": {
"text": {
"field": "Area (km2)",
"type": "quantitative",
"format": "0.3s"
}
}
}
]
},
{
"name": "INSET_CHART",
"title": {
"text": "Inset Chart",
"font": "Segoe UI",
"fontSize": 14,
"fontStyle": "italic",
"fontWeight": "bold",
"subtitle": "Truncated Range (end omitted for outliers; easier to see differences between non-outlier values)",
"subtitleFont": "Segoe UI",
"subtitleFontSize": 12,
"subtitleFontWeight": "normal",
"subtitleFontStyle": "italic"
},
// TO DO: investigate how to use an expression of width here
"width": 500,
"height": 470,
"encoding": {
"y": {
"field": "_country_area_rank_country_name_label",
"type": "nominal",
"sort": "-x",
"axis": null
},
"x": {
"field": "Area (km2)",
"type": "quantitative",
"axis": {
"title": null,
"labelFlush": false,
"formatType": "pbiFormat",
"format": "0,.# k"
},
"scale": {
"domain": [
0,
{
"expr": "_inset_chart_upper_value2 + 50000"
}
]
}
}
},
"layer": [
{
"name": "INSET_BACKGROUND",
"mark": {
"type": "rect",
"color": "#E3E3E3",
"opacity": 0.25
},
"encoding": {
"x": {
"datum": 0
},
"x2": {
"datum": {
"expr": "_inset_chart_upper_value2 + 50000"
}
}
}
},
{
"name": "INSET_BAR",
"mark": {
"type": "bar",
"color": {
"expr": "_bar_colour"
},
"opacity": 0.6,
"height": {
"band": 0.8
},
// restrict the domain to the specified range
"clip": true
}
},
{
"name": "INSET_GRADIENT_BAR",
"transform": [
{
"filter": "datum['_country_area_rank'] <= _outliers"
}
],
"mark": {
"type": "bar",
"color": {
"x1": 0,
"y1": 0,
"x2": 1,
"y2": 0,
"gradient": "linear",
"stops": [
{
"offset": 0,
// hard-coded colour: Vega-Lite expects string, not parameter
"color": "#01322000"
},
{
"offset": 1,
"color": "white"
}
]
},
"height": {
"band": 0.8
},
// restrict the domain to the specified range
"clip": true
},
"encoding": {
"x": {
"datum": {
"expr": "_inset_chart_upper_value2"
}
},
"x2": {
"datum": {
"expr": "_inset_chart_upper_value2 + 50000"
}
}
}
},
{
"name": "INSET_DATA_LABEL",
"transform": [
{
"calculate": "if( datum['_country_area_rank'] > _outliers, true, false )",
"as": "_above_outlier_rank"
}
],
"mark": {
"type": "text",
"align": {
"expr": "if( datum['_above_outlier_rank'] == true, 'left', 'right' )"
},
"xOffset": {
"expr": "if( datum['_above_outlier_rank'] == true, 4, -4 )"
},
"font": "Segoe UI",
"color": {
"expr": "if( datum['_above_outlier_rank'] == true, 'black', 'white' )"
}
},
"encoding": {
"text": {
"field": "Area (km2)",
"type": "quantitative",
"format": "0.3s"
},
"x": {
"field": "_inset_chart_country_area",
"type": "quantitative"
}
}
}
]
}
]
}
NOTE: The use of both axis data labels and bar data labels is not a recommended design choice but was included for illustrative purposes only.
Also included is the development sample PBIX using sample data from Wikipedia for both 100 meter times and country areas.
The intent of these examples were not to provide finished visuals, but rather to explore the use of the Deneb custom visual and the Vega-Lite language within Power BI and to serve as a starting point for further development.
These examples are provided as-is for information purposes only, and their use is solely at the discretion of the end user; no responsibility is assumed by the author.
Greg
Deneb Example - Inset Charts - V8.pbix (4.6 MB)