Power BI Dumbbell Chart (Wykres hantlowy) w Deneb i Vega-Lite: Porównanie danych bez kompromisów 🚀

Wykres hantlowy w Power BI (ang. dumbbell chart, znany również jako wykres DNA lub sztangowy) to jedno z najbardziej czytelnych rozwiązań wizualnych do porównywania dwóch punktów danych w ramach różnych kategorii. Standardowe wizualizacje w Power BI często zmuszają do korzystania z przeładowanych, zgrupowanych wykresów słupkowych lub osobnych tabel przestawnych, aby pokazać odchylenia między bieżącym a poprzednim okresem.

Dzięki wykorzystaniu dodatku Deneb i języka Vega-Lite możesz stworzyć w Power BI w pełni spersonalizowany wykres hantlowy. Oferuje on automatyczny ranking Top 10, bezkolizyjne etykiety danych, czytelne dla człowieka jednostki liczb (tys. i mln) oraz wbudowaną kolumnę odchyleń (DIFF). ✨

Dlaczego warto wybrać wykres hantlowy w Power BI? 💡

  • 🎯 Zero chaosu: Wyświetla bieżący rok (CY) i poprzedni rok (PY) na jednej poziomej osi.
  • 📊 Błyskawiczny wgląd w odchylenia: Dedykowany, kontrastowy pasek DIFF tuż obok wykresu.
  • 🛡️ Inteligentne unikanie nakładania się tekstu: Gdy punkty leżą zbyt blisko siebie, etykieta poprzedniego okresu dynamicznie się ukrywa, eliminując ryzyko nachodzenia na siebie liczb.
  • 🔢 Automatyczne formatowanie liczb: Skrótowe wartości zaokrąglone do pełnych jednostek (tys., mln) bez zbędnych miejsc po przecinku.
  • 🔄 W 100% gotowy do ponownego użycia szablon: Wystarczy przeciągnąć 3 pola (Kategoria, Wartość bieżąca, Wartość poprzednia), a wizualizacja skonfiguruje się sama w ułamku sekundy.

Instrukcja wdrożenia krok po kroku 🛠️

Krok 1: Dodaj wizualizację Deneb 📦

  1. W programie Power BI Desktop przejdź do panelu Wizualizacje (Visualizations).
  2. Kliknij ikonę wielokropka (…) i wybierz Pobierz więcej wizualizacji (Get more visuals).
  3. Wyszukaj Deneb: Declarative Visualization in Power BI i kliknij Dodaj (Add).

Krok 2: Przypisz i zmień nazwy pól 🏷️

  1. Dodaj wizualizację Deneb do kanwy raportu.
  2. Przeciągnij swoje 3 pola do sekcji Wartości (Values) wizualizacji.
  3. Zmień nazwy pól bezpośrednio w wizualizacji (kliknij prawym przyciskiem myszy na pole –>Zmień nazwę dla tej wizualizacji / Rename for this visual):
    • Kolumna z kategorią –>Category
    • Główna/bieżąca miara –>Current Value
    • Miara porównawcza/poprzednia –>Prev Value

Krok 3: Wklej specyfikację Vega-Lite 💻

  1. Kliknij menu … w prawym górnym rogu wizualizacji i wybierz Edytuj (Edit).
  2. W zakładce specyfikacji zastąp cały dotychczasowy kod poniższym kodem JSON:

JSON

 {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {"name": "dataset"},
  "spacing": 1,
  "padding": {"top": 22, "left": 5, "right": 10, "bottom": 5},
  "config": {
    "font": "Segoe UI, sans-serif"
  },
  "transform": [
    {
      "filter": "datum['Category'] != null && datum['Category'] != ''"
    },
    {
      "window": [{"op": "row_number", "as": "rank"}],
      "sort": [{"field": "Current Value", "order": "descending"}]
    },
    {
      "filter": "datum.rank <= 10"
    },
    {
      "calculate": "datum['Current Value'] - datum['Prev Value']",
      "as": "Diff"
    },
    {
      "calculate": "(datum['Diff'] > 0 ? '▲ +' : datum['Diff'] < 0 ? '▼ -' : '') + (abs(datum['Diff']) >= 1000000 ? format(abs(datum['Diff']) / 1000000, ',.0f') + ' mln' : abs(datum['Diff']) >= 1000 ? format(abs(datum['Diff']) / 1000, ',.0f') + ' k' : format(abs(datum['Diff']), ',.0f'))",
      "as": "Diff_Formatted"
    },
    {
      "calculate": "abs(datum['Current Value']) >= 1000000 ? format(datum['Current Value'] / 1000000, ',.0f') + ' mln' : abs(datum['Current Value']) >= 1000 ? format(datum['Current Value'] / 1000, ',.0f') + ' k' : format(datum['Current Value'], ',.0f')",
      "as": "CY_Formatted"
    },
    {
      "calculate": "abs(datum['Prev Value']) >= 1000000 ? format(datum['Prev Value'] / 1000000, ',.0f') + ' mln' : abs(datum['Prev Value']) >= 1000 ? format(datum['Prev Value'] / 1000, ',.0f') + ' k' : format(datum['Prev Value'], ',.0f')",
      "as": "PY_Formatted_Base"
    },
    {
      "calculate": "datum['Current Value'] != 0 && abs(datum['Diff']) / abs(datum['Current Value']) < 0.15 ? '' : datum['PY_Formatted_Base']",
      "as": "PY_Formatted"
    }
  ],
  "hconcat": [
    {
      "width": 380,
      "encoding": {
        "y": {
          "field": "Category",
          "type": "nominal",
          "scale": {
            "step": 44,
            "paddingOuter": 1.0
          },
          "axis": {
            "title": null,
            "domain": false,
            "ticks": false,
            "labelPadding": 12,
            "labelFontSize": 11,
            "labelFontWeight": 300,
            "labelColor": "#333333"
          },
          "sort": {"field": "Current Value", "op": "max", "order": "descending"}
        }
      },
      "layer": [
        {
          "name": "PASEK_LACZACY",
          "mark": {
            "type": "rule",
            "color": "#E5E5E5",
            "strokeWidth": 2
          },
          "encoding": {
            "x": {
              "field": "Prev Value",
              "type": "quantitative"
            },
            "x2": {
              "field": "Current Value"
            }
          }
        },
        {
          "name": "KULKA_PY",
          "mark": {
            "type": "circle",
            "size": 110
          },
          "encoding": {
            "x": {
              "field": "Prev Value",
              "type": "quantitative",
              "axis": {
                "grid": true,
                "gridColor": "#F2F2F2",
                "domain": false,
                "ticks": false,
                "title": null,
                "labelFontSize": 9,
                "labelFontWeight": 300,
                "labelColor": "#888888",
                "format": "~s"
              }
            },
            "color": {
              "datum": "Previous Value",
              "type": "nominal",
              "scale": {
                "domain": ["Current Value", "Previous Value"],
                "range": ["#ED8A18", "#8C8C8C"]
              },
              "legend": {
                "orient": "top",
                "direction": "horizontal",
                "title": null,
                "labelFontSize": 11,
                "labelFontWeight": 300,
                "symbolSize": 80,
                "padding": 5,
                "offset": 12
              }
            }
          }
        },
        {
          "name": "ETYKIETA_PY",
          "mark": {
            "type": "text",
            "dy": 11,
            "color": "#666666",
            "fontSize": 8.5,
            "fontWeight": 300,
            "font": "Segoe UI"
          },
          "encoding": {
            "x": {"field": "Prev Value", "type": "quantitative"},
            "text": {"field": "PY_Formatted"}
          }
        },
        {
          "name": "KULKA_CY",
          "mark": {
            "type": "circle",
            "size": 110
          },
          "encoding": {
            "x": {"field": "Current Value", "type": "quantitative"},
            "color": {
              "datum": "Current Value",
              "type": "nominal"
            }
          }
        },
        {
          "name": "ETYKIETA_CY",
          "mark": {
            "type": "text",
            "dy": -11,
            "color": "#D37300",
            "fontSize": 8.5,
            "fontWeight": 300,
            "font": "Segoe UI"
          },
          "encoding": {
            "x": {"field": "Current Value", "type": "quantitative"},
            "text": {"field": "CY_Formatted"}
          }
        }
      ]
    },
    {
      "width": 95,
      "title": {
        "text": "DIFF",
        "fontSize": 10,
        "fontWeight": 400,
        "color": "#555555"
      },
      "encoding": {
        "y": {
          "field": "Category",
          "type": "nominal",
          "axis": null,
          "scale": {
            "step": 44,
            "paddingOuter": 1.0
          },
          "sort": {"field": "Current Value", "op": "max", "order": "descending"}
        }
      },
      "layer": [
        {
          "name": "TLO_DIFF",
          "mark": {
            "type": "rect",
            "color": "#F4F3EF"
          }
        },
        {
          "name": "WARTOSC_DIFF",
          "mark": {
            "type": "text",
            "fontWeight": 400,
            "fontSize": 10,
            "font": "Segoe UI"
          },
          "encoding": {
            "text": {"field": "Diff_Formatted"},
            "color": {
              "condition": [
                {
                  "test": "datum['Diff'] > 0",
                  "value": "#137333"
                },
                {
                  "test": "datum['Diff'] < 0",
                  "value": "#C5221F"
                }
              ],
              "value": "#666666"
            }
          }
        }
      ]
    }
  ],
  "resolve": {
    "scale": {
      "y": "shared"
    }
  }
}
  1. Naciśnij Zastosuj (Apply / Ctrl + Enter) i kliknij Wróć do raportu (Back to report). ✅

Personalizacja wykresu ⚙️

  • 🔝 Zmień limit Top N: Zmodyfikuj "datum.rank <= 10" w sekcji transform (np. ustaw wartość na 5 lub 15).
  • 📏 Dostosuj odstępy między wierszami: Zmień wartość w "scale": {"step": 36}, aby zagęścić lub rozsunąć wiersze.
  • 🎨 Dopasuj paletę barw: Zaktualizuj kody kolorów HEX w "range": ["#ED8A18", "#8C8C8C"], aby dopasować je do identyfikacji wizualnej swojej firmy.

Ten spersonalizowany szablon wykresu hantlowego w Power BI przekształca rutynowe porównania okres do okresu w profesjonalne wizualizacje gotowe dla kadry zarządzającej – bez konieczności ciągłej obsługi. Skopiuj kod JSON, przypisz swoje miary i błyskawicznie przenieś design swoich dashboardów na wyższy poziom. 📈🔥