Tired of standard Power BI bar charts that fail to show how your data actually flows? 😴 Whether you are analyzing spend allocation, product profitability, or customer journeys, a multi-level Sankey Flow Chart is the ultimate secret weapon for executive-level storytelling.
In this quick tutorial, you’ll learn how to create a highly responsive, publication-ready Product –>Category –> Total Spend flow chart in Power BI using the free Deneb custom visual.
Best part? It automatically ranks your top items, bundles the rest into a clean „Other” category, and fits seamlessly into any modern corporate dashboard! 💎

Table of Contents
🛠️ What You Need Sankey Flow Chart
To follow along, you only need:
- Power BI Desktop
- The Deneb visual (Free from Microsoft AppSource)
- 3 data fields: Product, Category, and a Value Measure (e.g., Sales, Total Spend, or COGS)
🚀 Step-by-Step Setup Guide (It Takes < 2 Minutes!)
Step 1: Import the Deneb Visual 🛍️
- Open your Power BI report.
- In the Visualizations pane, click the three dots (
...) and choose Get more visuals. - Search for Deneb (declarative visualization spec built on Vega/Vega-Lite) and click Add.
Step 2: Add Your Data Fields 📥
Drag the Deneb visual onto your canvas and drop three fields into the Values field bucket:
- Product field (e.g.,
Product Name,Product, orProdukt) - Category field (e.g.,
Category,Kategoria, orGroup) - Value / Numeric measure (e.g.,
Total COGS,Value,Sales, orAmount)
💡 Smart Feature: The code includes built-in auto-detection for common English and Polish column names, so you don’t even need to rename your model fields!
Step 3: Paste the Vega Code 🪄
- Hover over the visual and click the top-right menu icon –> Edit.
- When prompted to select a specification framework:
- Select Vega (⚠️ Important: Choose Vega, not Vega-Lite!).
- Pick [empty template].
- Copy the full JSON code below and paste it into the code editor on the left.
- Click Apply Changes (or press
Ctrl+Enter).
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 850,
"height": 520,
"padding": {"top": 20, "left": 15, "right": 20, "bottom": 20},
"signals": [
{"name": "topN", "value": 5},
{"name": "leftX", "value": 240},
{"name": "midX", "value": 480},
{"name": "rightX", "value": 660},
{"name": "topMargin", "value": 45},
{"name": "pGap", "value": 8},
{"name": "cGap", "value": 30},
{"name": "targetY", "value": 175}
],
"data": [
{
"name": "dataset"
},
{
"name": "raw_data",
"source": "dataset",
"transform": [
{
"type": "formula",
"expr": "datum.Product != null ? datum.Product : datum['Product Name'] != null ? datum['Product Name'] : datum.Produkt != null ? datum.Produkt : 'Product'",
"as": "p_raw"
},
{
"type": "formula",
"expr": "datum.Category != null ? datum.Category : datum.Kategoria != null ? datum.Kategoria : 'Category'",
"as": "c_raw"
},
{
"type": "formula",
"expr": "toNumber(datum.Value != null ? datum.Value : datum.Wartosc != null ? datum.Wartosc : datum['Total COGS'] != null ? datum['Total COGS'] : 0)",
"as": "val"
}
]
},
{
"name": "prod_totals",
"source": "raw_data",
"transform": [
{
"type": "aggregate",
"groupby": ["p_raw"],
"ops": ["sum"],
"fields": ["val"],
"as": ["p_tot"]
},
{
"type": "window",
"sort": {"field": "p_tot", "order": "descending"},
"ops": ["row_number"],
"as": ["p_rank"]
}
]
},
{
"name": "ranked_data",
"source": "raw_data",
"transform": [
{
"type": "lookup",
"from": "prod_totals",
"key": "p_raw",
"fields": ["p_raw"],
"values": ["p_rank"]
},
{
"type": "formula",
"expr": "datum.p_rank <= topN ? datum.p_rank : (topN + 1)",
"as": "p_sort"
},
{
"type": "formula",
"expr": "datum.p_rank <= topN ? datum.p_rank + '. ' + datum.p_raw : (topN + 1) + '. Other'",
"as": "p_name"
}
]
},
{
"name": "grand_total",
"source": "ranked_data",
"transform": [
{
"type": "aggregate",
"ops": ["sum"],
"fields": ["val"],
"as": ["tot_val"]
}
]
},
{
"name": "c_nodes",
"source": "ranked_data",
"transform": [
{
"type": "aggregate",
"groupby": ["c_raw"],
"ops": ["sum"],
"fields": ["val"],
"as": ["c_val"]
},
{
"type": "window",
"sort": {"field": "c_val", "order": "descending"},
"ops": ["row_number"],
"as": ["c_rank"]
},
{
"type": "stack",
"groupby": [],
"field": "c_val",
"sort": {"field": "c_val", "order": "descending"},
"as": ["cy0", "cy1"]
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.cy0) + (datum.c_rank - 1) * cGap",
"as": "c_top"
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.cy1) + (datum.c_rank - 1) * cGap",
"as": "c_bottom"
},
{
"type": "formula",
"expr": "(datum.c_top + datum.c_bottom) / 2",
"as": "c_mid"
},
{
"type": "formula",
"expr": "max(6, datum.c_bottom - datum.c_top)",
"as": "c_height"
}
]
},
{
"name": "p_nodes",
"source": "ranked_data",
"transform": [
{
"type": "aggregate",
"groupby": ["p_sort", "p_name"],
"ops": ["sum"],
"fields": ["val"],
"as": ["p_val"]
},
{
"type": "window",
"sort": {"field": "p_sort", "order": "ascending"},
"ops": ["row_number"],
"as": ["p_seq"]
},
{
"type": "stack",
"groupby": [],
"field": "p_val",
"sort": {"field": "p_sort", "order": "ascending"},
"as": ["py0", "py1"]
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.py0) + (datum.p_seq - 1) * pGap",
"as": "p_top"
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.py1) + (datum.p_seq - 1) * pGap",
"as": "p_bottom"
},
{
"type": "formula",
"expr": "(datum.p_top + datum.p_bottom) / 2",
"as": "p_mid"
},
{
"type": "formula",
"expr": "max(4, datum.p_bottom - datum.p_top)",
"as": "p_height"
}
]
},
{
"name": "p_c_links",
"source": "ranked_data",
"transform": [
{
"type": "aggregate",
"groupby": ["p_sort", "p_name", "c_raw"],
"ops": ["sum"],
"fields": ["val"],
"as": ["link_val"]
},
{
"type": "lookup",
"from": "c_nodes",
"key": "c_raw",
"fields": ["c_raw"],
"values": ["c_rank", "cy0"]
},
{
"type": "lookup",
"from": "p_nodes",
"key": "p_name",
"fields": ["p_name"],
"values": ["p_seq", "py0"]
},
{
"type": "stack",
"groupby": ["p_name"],
"sort": {"field": "c_rank", "order": "ascending"},
"field": "link_val",
"as": ["p_sy0", "p_sy1"]
},
{
"type": "stack",
"groupby": ["c_raw"],
"sort": {"field": "p_sort", "order": "ascending"},
"field": "link_val",
"as": ["c_sy0", "c_sy1"]
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.py0 + (datum.p_sy0 + datum.p_sy1)/2) + (datum.p_seq - 1) * pGap",
"as": "link_p_y"
},
{
"type": "formula",
"expr": "topMargin + scale('yScale', datum.cy0 + (datum.c_sy0 + datum.c_sy1)/2) + (datum.c_rank - 1) * cGap",
"as": "link_c_y"
},
{
"type": "formula",
"expr": "max(1.5, scale('yScale', datum.link_val))",
"as": "link_width"
}
]
}
],
"scales": [
{
"name": "yScale",
"type": "linear",
"domain": {"data": "grand_total", "field": "tot_val"},
"range": [0, 260],
"zero": true
},
{
"name": "catColor",
"type": "ordinal",
"domain": {"data": "c_nodes", "field": "c_raw"},
"range": [
"#0284C7", "#00C896", "#F43F5E", "#0F172A", "#64748B",
"#3B82F6", "#10B981", "#E11D48", "#8B5CF6", "#475569"
]
}
],
"marks": [
{
"type": "text",
"encode": {
"enter": {
"x": {"signal": "leftX - 10"},
"y": {"value": 20},
"align": {"value": "right"},
"fontSize": {"value": 11},
"fontWeight": {"value": "bold"},
"fill": {"value": "#475569"}
},
"update": {
"text": {"signal": "'PRODUCTS (TOP ' + topN + ' + OTHER)'"}
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": {"signal": "midX"},
"y": {"value": 20},
"align": {"value": "center"},
"fontSize": {"value": 11},
"fontWeight": {"value": "bold"},
"fill": {"value": "#475569"},
"text": {"value": "CATEGORIES"}
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": {"signal": "rightX + 22"},
"y": {"value": 20},
"align": {"value": "left"},
"fontSize": {"value": 11},
"fontWeight": {"value": "bold"},
"fill": {"value": "#475569"},
"text": {"value": "TOTAL SPEND"}
}
}
},
{
"type": "path",
"from": {"data": "p_c_links"},
"encode": {
"update": {
"path": {
"signal": "'M ' + (leftX + 6) + ' ' + datum.link_p_y + ' C ' + (leftX + 100) + ' ' + datum.link_p_y + ', ' + (midX - 100) + ' ' + datum.link_c_y + ', ' + (midX - 8) + ' ' + datum.link_c_y"
},
"stroke": {"scale": "catColor", "field": "c_raw"},
"strokeWidth": {"field": "link_width"},
"strokeOpacity": {"value": 0.40},
"strokeCap": {"value": "round"}
}
}
},
{
"type": "path",
"from": {"data": "c_nodes"},
"encode": {
"update": {
"path": {
"signal": "'M ' + (midX + 8) + ' ' + datum.c_mid + ' C ' + (midX + 80) + ' ' + datum.c_mid + ', ' + (rightX - 50) + ' ' + targetY + ', ' + rightX + ' ' + targetY"
},
"stroke": {"scale": "catColor", "field": "c_raw"},
"strokeWidth": {"field": "c_height"},
"strokeOpacity": {"value": 0.35},
"strokeCap": {"value": "round"}
}
}
},
{
"type": "rect",
"from": {"data": "p_nodes"},
"encode": {
"update": {
"x": {"signal": "leftX - 6"},
"y": {"field": "p_top"},
"width": {"value": 12},
"height": {"field": "p_height"},
"fill": {"signal": "datum.p_sort > topN ? '#94A3B8' : '#0F172A'"},
"cornerRadius": {"value": 3}
}
}
},
{
"type": "text",
"from": {"data": "p_nodes"},
"encode": {
"enter": {
"align": {"value": "right"},
"baseline": {"value": "middle"},
"fontSize": {"value": 10},
"fontWeight": {"value": "bold"},
"fill": {"value": "#1E293B"}
},
"update": {
"x": {"signal": "leftX - 14"},
"y": {"field": "p_mid"},
"text": {"signal": "datum.p_name + ' (' + format(datum.p_val, ',.0f') + ')'"}
}
}
},
{
"type": "rect",
"from": {"data": "c_nodes"},
"encode": {
"update": {
"x": {"signal": "midX - 8"},
"y": {"field": "c_top"},
"width": {"value": 16},
"height": {"field": "c_height"},
"fill": {"scale": "catColor", "field": "c_raw"},
"cornerRadius": {"value": 3}
}
}
},
{
"type": "text",
"from": {"data": "c_nodes"},
"encode": {
"enter": {
"align": {"value": "center"},
"baseline": {"value": "bottom"},
"fontSize": {"value": 10},
"fontWeight": {"value": "bold"},
"fill": {"value": "#1E293B"}
},
"update": {
"x": {"signal": "midX"},
"y": {"signal": "datum.c_top - 4"},
"text": {"signal": "datum.c_raw + ' (' + format(datum.c_val, ',.0f') + ')'"}
}
}
},
{
"type": "rect",
"from": {"data": "grand_total"},
"encode": {
"update": {
"x": {"signal": "rightX"},
"y": {"signal": "targetY - 30"},
"width": {"value": 12},
"height": {"value": 60},
"fill": {"value": "#0F172A"},
"cornerRadius": {"value": 4}
}
}
},
{
"type": "text",
"from": {"data": "grand_total"},
"encode": {
"enter": {
"align": {"value": "left"},
"baseline": {"value": "bottom"},
"fontSize": {"value": 13},
"fontWeight": {"value": "bold"},
"fill": {"value": "#0F172A"}
},
"update": {
"x": {"signal": "rightX + 22"},
"y": {"signal": "targetY - 2"},
"text": {"signal": "format(datum.tot_val, ',.0f')"}
}
}
},
{
"type": "text",
"from": {"data": "grand_total"},
"encode": {
"enter": {
"align": {"value": "left"},
"baseline": {"value": "top"},
"fontSize": {"value": 10},
"fill": {"value": "#64748B"},
"text": {"value": "Total Budget"}
},
"update": {
"x": {"signal": "rightX + 22"},
"y": {"signal": "targetY + 2"}
}
}
}
]
}
Step 4: Tweak & Customize Like a Pro ⚙️
Want to tune the visual specifically for your report theme? You can easily adjust the Signals at the top of the code:
- 🔝 Change TOP N Items: Look for
"topN": 5in the signals block. Change it to3,8, or10depending on how many top products you want to highlight before grouping the rest into"Other". - 🎨 Color Palette: Match your exact brand design by editing the hex color codes inside the
"catColor"scale array!
🎯 Why Your Readers & Stakeholders Will Love This Chart:
- ✅ 100% Data Integrity: Never drops long-tail data—top performers shine while smaller items seamlessly condense into an „Other” node.
- ✅ Zero Overlap: Auto-calculated vertical offsets ensure category labels and flow ribbons never collide or cut off.
- ✅ Pixel-Perfect Alignment: All flows stream gracefully into a clean, compact Total Budget anchor node.
💬 Over to You!
Did you try this in your Power BI report? Let us know in the comments below what data flows you’re analyzing—and don’t forget to share a screenshot of your beautiful dashboard! 📊✨
