Masz dość standardowych wykresów słupkowych w Power BI, które nie potrafią pokazać, jak w rzeczywistości przepływają Twoje dane? 😴 Niezależnie od tego, czy analizujesz alokację wydatków, rentowność produktów czy ścieżki klientów, wielopoziomowy wykres Sankeya to absolutna tajna broń w raportowaniu dla kadry zarządzającej.
W tym krótkim poradniku dowiesz się, jak stworzyć w pełni elastyczny, gotowy do publikacji wykres przepływu Produkt –> Kategoria –> Wydatki ogółem w Power BI za pomocą darmowej wizualizacji niestandardowej Deneb.
Najlepsza część? Wykres automatycznie tworzy ranking najważniejszych pozycji, resztę estetycznie grupuje w kategorii „Other” i idealnie wpasowuje się w każdy nowoczesny dashboard firmowy! 💎

Table of Contents
🛠️ Czego potrzebujesz do przygotowania wykresu Sankeya
Aby przejść przez ten poradnik, potrzebujesz jedynie:
- Power BI Desktop
- Wizualizacji Deneb (darmowa, dostępna w Microsoft AppSource)
- 3 pól danych: Product, Category, oraz miary Value (np. Sprzedaż, Wydatki ogółem lub Koszt własny sprzedaży – COGS)
🚀 Instrukcja wdrożenia krok po kroku (zajmie Ci to < 2 minuty!)
Krok 1: Zaimportuj wizualizację Deneb 🛍️
- Otwórz swój raport w Power BI.
- W okienku Wizualizacje (Visualizations) kliknij ikonę wielokropka (…) i wybierz Pobierz więcej wizualizacji (Get more visuals).
- Wyszukaj Deneb (deklaratywna specyfikacja wizualizacji oparta na językach Vega/Vega-Lite) i kliknij Dodaj (Add).
Krok 2: Dodaj swoje pola danych 📥
Przeciągnij wizualizację Deneb na kanwę raportu i upuść trzy pola w sekcji Wartości (Values):
- Pole produktu (np. Nazwa produktu, Product lub Produkt)
- Pole kategorii (np. Kategoria, Category lub Grupa)
- Miarę wartości / miarę liczbową (np. Koszt własny sprzedaży – COGS, Wartość, Sprzedaż lub Kwota)
💡 Sprytna funkcja: Kod ma wbudowane automatyczne wykrywanie popularnych angielskich i polskich nazw kolumn, więc nie musisz nawet zmieniać nazw pól w swoim modelu!
Krok 3: Wklej kod Vega 🪄
- Najedź kursorem na wizualizację i kliknij ikonę menu w prawym górnym rogu $\rightarrow$Edytuj (Edit).
- Po wyświetleniu monitu o wybór frameworka specyfikacji:
- Wybierz Vega (⚠️ Ważne: Wybierz Vega, nie Vega-Lite!).
- Wybierz [empty template](pusty szablon).
- Skopiuj poniższy, kompletny kod JSON i wklej go do edytora kodu po lewej stronie.
- Kliknij Zastosuj zmiany (Apply Changes lub naciśnij 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"}
}
}
}
]
}
Krok 4: Dostosuj wykres jak zawodowiec ⚙️
Chcesz precyzyjnie dopasować wizualizację do motywu swojego raportu? Możesz z łatwością zmodyfikować sekcję sygnałów (Signals) na samej górze kodu:
- 🔝 Zmień liczbę elementów TOP N: Znajdź parametr
"topN": 5w bloku sygnałów. Zmień wartość na 3, 8 lub 10 w zależności od tego, ile kluczowych produktów chcesz wyeksponować przed zgrupowaniem pozostałych w kategorii „Pther”. - 🎨 Paleta kolorów: Dopasuj barwy idealnie do identyfikacji wizualnej swojej marki, edytując kody kolorów HEX w tablicy skali
"catColor"!
🎯 Dlaczego Twoi odbiorcy i interesariusze pokochają ten wykres:
- ✅ 100% integralności danych: Nigdy nie pomija danych z „długiego ogona” – najważniejsze pozycje są wyeksponowane, podczas gdy mniejsze wartości płynnie łączą się w węzeł „Other”.
- ✅ Zero nakładania się elementów: Automatycznie kalkulowane przesunięcia pionowe gwarantują, że etykiety kategorii i wstęgi przepływu nigdy się nie zderzają ani nie ucinają.
- ✅ Wyrównanie co do piksela: Wszystkie strumienie zbiegają się płynnie w estetyczny, zwięzły węzeł końcowy z sumą budżetu (Total Budget).
💬 Teraz Twoja kolej!
Wypróbowałeś to rozwiązanie w swoim raporcie Power BI? Daj znać w komentarzach poniżej, jakie przepływy danych analizujesz – i koniecznie pochwal się zrzutem ekranu swojego nowego dashboardu! 📊✨
