Deneb/Vega-Lite can be used to switch between visuals. The idea for this was discussed among the Deneb Community Experts and the Vega-Lite syntax to achieve this was first developed by Madison Giammaria and posted to LinkedIn earlier this week.
Here’s another version that leverages Power BI for a text box (title), shapes (background rectangle with shadow; slicer separator vertical line), and slicers (period; chart type); this version presents simple financial data (from Yahoo Finance, in this case for Microsoft) for stock price and volume.
This example illustrates a number of Deneb/Vega-Lite features, including:
0 - General:
- a “params” block with:
- 2x parameters for the positive and negative colours
- an expression to retrieve the chart type selected in the Power BI slicer
- an expression to adjust the top-and-bottom “padding” of the child visuals (price area chart, volume column chart)
- a shared “transform” block to extend the dataset in-visual with:
- 2x “calculate” transforms to set/retrieve the maximum date from the dataset and to use the period selected in the Power BI slicer to calculate the minimum date,
- a “filter” transform to restrict the dataset to the period of interest
- a “window\rank” transform using the internal row number to allow easy reference to the “first” record
- a “window\lag” transform to allow easy reference to the previous price
- 2x “calculate” transforms to determine the price variance amount and percentage
- a “window\lag” transform to allow easy reference to the previous volume
- 2x “calculate” transforms to determine the volume variance amount and percentage
- a “vconcat” block to vertically concatenate the child visuals (price area chart, volume column chart)
1 - Price:
- a “layer” block to allow for shared X encoding
- a shared “encoding” block for the X axis (temporal, by date)
- a nested “layer” block with:
- an “area” mark for the daily [close] price with dark line, gradient colours, and custom tooltip
- a nested “transform” block with:
- a “window\last_value” transform to determine the last (period) price
- a “window\first_value” transform to determine the first (period) price
- a “filter” transform to restrict the dataset to a single record (using the rank calculated above)
- 2x “calculate” transforms to determine the (last - first) price variance amount and percentage
- 2x “text” marks for the last price and variance percent (displayed in colour: green for positive, red for negative)
- 4x “text” marks for the first price and labels (displayed in black)
- a “rule” mark and an “point\triangle” mark for the variance direction arrow (both displayed in colour: green for positive, red for negative)
2 - Volume:
- a “layer” block to allow for shared X encoding
- a shared “encoding” block for the X axis (temporal, by date)
- a nested “layer” block with:
- a “bar” mark for the daily volume with variances displayed in colour (green for positive, red for negative) and custom tooltip
- a nested “transform” block with:
- a “window\last_value” transform to determine the last (period) volume
- a “window\first_value” transform to determine the first (period) volume
- a “filter” transform to restrict the dataset to a single record (using the rank calculated above)
- 2x “calculate” transforms to determine the (last - first) volume variance amount and percentage
- a “text” mark for the last volume (displayed in colour: green for positive, red for negative)
- 3x “text” marks for the first volume and labels (displayed in black)
Deneb/Vega-Lite JSON Code:
{
"data": {
"name": "dataset"
},
"params": [
{
"name": "_positive_colour",
"value": "#09AA64"
},
{
"name": "_negative_colour",
"value": "#B51C36"
},
{
"name": "_chart_type",
"expr": "data('dataset')[0]['Type']"
},
{
"name": "padding",
"expr": "{top: _chart_type == 'Price' ? 20 : -600 + 10, bottom: _chart_type == 'Price' ? -600 - 2 : 60}"
}
],
"transform": [
{
"calculate": "toDate( '2024-07-12' )",
"as": "_max_date"
},
{
"calculate": "datum['Range'] == '5D' ? timeOffset( 'day', datum['_max_date'], -5 ) : datum['Range'] == '1M' ? timeOffset( 'month', datum['_max_date'], -1 ) : datum['Range'] == '3M' ? timeOffset( 'month', datum['_max_date'], -3 ) : datum['Range'] == '6M' ? timeOffset( 'month', datum['_max_date'], -6 ) : datum['Range'] == 'YTD' ? datetime( year( datum['_max_date'] ), 0, 1 ) : datum['Range'] == '1Y' ? timeOffset( 'year', datum['_max_date'], -1 ) : datum['Range'] == '3Y' ? timeOffset( 'year', datum['_max_date'], -3 ) : datum['Range'] == '5Y' ? timeOffset( 'year', datum['_max_date'], -5 ) : datum['Range'] == '10Y' ? timeOffset( 'year', datum['_max_date'], -10 ) : datum['Range'] == 'ALL' ? timeOffset( 'year', datum['_max_date'], -500 ) : null",
"as": "_min_date"
},
{
"filter": "datum['Date'] <= datum['_max_date'] && datum['Date'] >= datum['_min_date']"
},
{
"window": [
{
"op": "rank",
"as": "_rank"
}
],
"sort": [
{
"field": "__row__",
"order": "ascending"
}
]
},
{
"calculate": "1 * datum['Close']",
"as": "_current_close"
},
{
"window": [
{
"op": "lag",
"field": "_current_close",
"as": "_previous_close"
}
]
},
{
"calculate": "datum['_current_close'] - datum['_previous_close']",
"as": "_close_variance"
},
{
"calculate": "datum['_close_variance'] / datum['_previous_close']",
"as": "_close_variance_percent"
},
{
"calculate": "1 * datum['Volume']",
"as": "_current_volume"
},
{
"window": [
{
"op": "lag",
"field": "_current_volume",
"as": "_previous_volume"
}
]
},
{
"calculate": "datum['_current_volume'] - datum['_previous_volume']",
"as": "_volume_variance"
},
{
"calculate": "datum['_volume_variance'] / datum['_previous_volume']",
"as": "_volume_variance_percent"
}
],
"vconcat": [
{
"name": "UPPER_PRICE",
"layer": [
{
"name": "PRICE_1",
"width": 1100,
"height": 540,
"encoding": {
"x": {
"field": "Date",
"type": "temporal",
"axis": {
"orient": "bottom",
"grid": false,
"titleFont": "Segoe UI",
"titleFontSize": 16,
"labelFont": "Segoe UI",
"labelFontSize": 12
}
}
},
"layer": [
{
"name": "PRICE_11",
"mark": {
"type": "area",
"line": {
"color": "darkblue"
},
"color": {
"x1": 1,
"y1": 1,
"x2": 1,
"y2": 0,
"gradient": "linear",
"stops": [
{
"offset": 0,
"color": "white"
},
{
"offset": 1,
"color": "darkblue"
}
]
}
},
"encoding": {
"y": {
"field": "Close",
"type": "quantitative",
"axis": {
"orient": "left",
"grid": false,
"title": "Price",
"titleFont": "Segoe UI",
"titleFontSize": 16,
"labelFont": "Segoe UI",
"labelFontSize": 12
},
"scale": {
"domainMax": 500
}
},
"tooltip": [
{
"field": "Date",
"type": "temporal",
"formatType": "pbiFormat",
"format": "dd-MMM-yyyy",
"title": "Date"
},
{
"field": "_current_close",
"type": "quantitative",
"format": ".4f",
"title": "Price"
},
{
"field": "_previous_close",
"type": "quantitative",
"format": ".4f",
"title": "Previous"
},
{
"field": "_close_variance_percent",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "+#,0.0%",
"title": "Variance"
}
]
}
}
]
},
{
"name": "PRICE_TEXT_1",
"transform": [
{
"window": [
{
"op": "last_value",
"field": "Close",
"as": "_last_close_price"
}
],
"frame": [
null,
null
]
},
{
"window": [
{
"op": "first_value",
"field": "Close",
"as": "_first_close_price"
}
],
"frame": [
null,
null
]
},
{
"filter": "datum['_rank'] == 1"
},
{
"calculate": "datum['_last_close_price'] - datum['_first_close_price']",
"as": "_variance"
},
{
"calculate": "datum['_variance'] / datum['_first_close_price']",
"as": "_variance_percent"
}
],
"layer": [
{
"name": "LAST_PRICE_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "bottom",
"fontSize": 48,
"x": 160,
"y": 60,
"color": {
"expr": "datum['_variance'] >= 0 ? _positive_colour : _negative_colour"
}
},
"encoding": {
"text": {
"field": "_last_close_price",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "#,0.00"
}
}
},
{
"name": "FIRST_PRICE_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "bottom",
"fontSize": 24,
"x": 260,
"y": 56,
"color": "black"
},
"encoding": {
"text": {
"field": "_first_close_price",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "#,0.00"
}
}
},
{
"name": "LAST_PRICE_LABEL_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "bottom",
"fontSize": 12,
"x": 160,
"y": 70,
"color": "black"
},
"encoding": {
"text": {
"value": "Last Price"
}
}
},
{
"name": "FIRST_PRICE_LABEL_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "bottom",
"fontSize": 12,
"x": 260,
"y": 70,
"color": "black"
},
"encoding": {
"text": {
"value": "First Price"
}
}
},
{
"name": "VERTICAL_RULE_1",
"mark": {
"type": "rule",
"strokeWidth": 4,
"color": {
"expr": "datum['_variance'] >= 0 ? _positive_colour : _negative_colour"
}
},
"encoding": {
"x": {
"value": 330
},
"y": {
"value": 10
},
"y2": {
"value": 60
}
}
},
{
"name": "VERTICAL_ARROW_1",
"mark": {
"type": "point",
"filled": true,
"size": 300,
"x": 330,
"y": {
"expr": "datum['_variance'] >= 0 ? 10 : 60"
},
"color": {
"expr": "datum['_variance'] >= 0 ? _positive_colour : _negative_colour"
},
"opacity": 1
},
"encoding": {
"shape": {
"condition": [
{
"test": "datum['_variance'] < 0",
"value": "triangle-down"
}
],
"value": "triangle-up"
}
}
},
{
"name": "VARIANCE_PERCENT_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "bottom",
"fontSize": 36,
"x": 460,
"y": 60,
"color": {
"expr": "datum['_variance'] >= 0 ? _positive_colour : _negative_colour"
}
},
"encoding": {
"text": {
"field": "_variance_percent",
"format": "+,.0%"
}
}
},
{
"name": "VARIANCE_PERCENT_LABEL_1",
"mark": {
"type": "text",
"align": "right",
"baseline": "top",
"fontSize": 12,
"x": 460,
"y": 60,
"color": "black"
},
"encoding": {
"text": {
"value": "Gain/Loss"
}
}
}
]
}
]
},
{
"name": "LOWER_VOLUME",
"layer": [
{
"name": "VOLUME_22",
"width": 1100,
"height": 540,
"encoding": {
"x": {
"field": "Date",
"type": "temporal",
"axis": {
"orient": "bottom",
"grid": false,
"titleFont": "Segoe UI",
"titleFontSize": 16,
"labelFont": "Segoe UI",
"labelFontSize": 12
}
}
},
"layer": [
{
"name": "VOLUME2",
"mark": {
"type": "bar",
"tooltip": true,
"color": {
"expr": "datum['_current_volume'] > datum['_previous_volume'] ? _positive_colour : _negative_colour"
}
},
"encoding": {
"y": {
"field": "Volume",
"type": "quantitative",
"axis": {
"orient": "left",
"grid": false,
"titleFont": "Segoe UI",
"titleFontSize": 16,
"labelFont": "Segoe UI",
"labelFontSize": 12,
"format": ".2s"
},
"scale": {
"domainMax": 200000000
}
},
"tooltip": [
{
"field": "Date",
"type": "temporal",
"formatType": "pbiFormat",
"format": "dd-MMM-yyyy",
"title": "Date"
},
{
"field": "_current_volume",
"type": "quantitative",
"title": "Volume"
},
{
"field": "_previous_volume",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "#,#",
"title": "Previous"
},
{
"field": "_volume_variance_percent",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "+#,0.0%",
"title": "Variance"
}
]
}
},
{
"name": "VOLUME_TEXT_2",
"transform": [
{
"window": [
{
"op": "last_value",
"field": "Volume",
"as": "_last_close_volume"
}
],
"frame": [
null,
null
]
},
{
"window": [
{
"op": "first_value",
"field": "Volume",
"as": "_first_close_volume"
}
],
"frame": [
null,
null
]
},
{
"filter": "datum['_rank'] == 1"
},
{
"calculate": "datum['_last_close_volume'] - datum['_first_close_volume']",
"as": "_variance"
},
{
"calculate": "datum['_variance'] / datum['_first_close_volume']",
"as": "_variance_percent"
}
],
"layer": [
{
"name": "LAST_VOLUME_2",
"mark": {
"type": "text",
"align": "left",
"baseline": "bottom",
"fontSize": 40,
"x": 200,
"y": 60,
"color": {
"expr": "datum['_variance'] >= 0 ? _positive_colour : _negative_colour"
}
},
"encoding": {
"text": {
"field": "_last_close_volume",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "#,0"
}
}
},
{
"name": "FIRST_VOLUME_2",
"mark": {
"type": "text",
"align": "left",
"baseline": "bottom",
"fontSize": 24,
"x": 200,
"y": 110,
"color": "black"
},
"encoding": {
"text": {
"field": "_first_close_volume",
"type": "quantitative",
"formatType": "pbiFormat",
"format": "#,0"
}
}
},
{
"name": "LAST_VOLUME_LABEL_2",
"mark": {
"type": "text",
"align": "left",
"baseline": "bottom",
"fontSize": 12,
"x": 200,
"y": 70,
"color": "black"
},
"encoding": {
"text": {
"value": "Last Volume"
}
}
},
{
"name": "FIRST_VOLUME_LABEL_2",
"mark": {
"type": "text",
"align": "left",
"baseline": "bottom",
"fontSize": 12,
"x": 200,
"y": 120,
"color": "black"
},
"encoding": {
"text": {
"value": "First Volume"
}
}
}
]
}
]
}
]
}
]
}
The intent of this example is not to provide a finished visual, 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.
Also included is the development PBIX using data for Microsoft’s stock price and volume from Yahoo Finance, downloaded 2024-07-12.
This example is provided as-is for information purposes only, and its use is solely at the discretion of the end user; no responsibility is assumed by the author.
Greg
Deneb Example - Period Stock Charts.pbix (4.1 MB)