Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Stapelgrafik

Flexible Stapel-Charts für VB- und WPF-Anwendungen.
© dotnetpro
Stapel-Charts veranschaulichen die Größenverhältnisse zwischen zusammengehörigen Werten. Ein Beispiel aus dem Finanzbereich sind die sofort oder in wenigen Tagen verfügbaren Gelder, also die Liquidität. Dazu gehören neben dem Kassenbestand die Geldbestände auf dem Girokonto, aber auch Tagesgelder, die, wenn gewünscht, schon am nächsten Bankarbeitstag verfügbar sind. In Bild 1 sehen Sie ein fiktives Beispiel dafür.
Beispiel einer einfachen Stapelgrafik (Bild 1) © Autor
Die hier beschriebenen Stapel-Charts sind wie die Donut-Charts in [1] ausschließlich für den Desktop gedacht und mit der Kombination von Windows Presentation Foundation (WPF) und Visual Basic .NET unter .NET 8 geschrieben. Auch die Hauptziele sind dieselben geblieben: Man soll das Chart-Modul mit möglichst wenigen Eingaben nutzen können. ­Alle Größenrelationen werden abhängig von der Canvas-Größe automatisch gesetzt, lassen sich bei Bedarf aber auch verändern. Außerdem soll eine ungünstige Reihenfolge der Werte per Mausklick korrigiert werden können, und die Farben soll der Anwender per Mausrad steuern können.Die Beispielanwendung hat zwei Leinwände namens cvLinks und cvRechts, die im XAML-Code definiert werden:

...
  <StackPanel Orientation="Horizontal" 
      Margin="30" Height="550">
    <Canvas Name="cvLinks" Margin="10 0 0 0" 
      Width="550" Height="650"
      Background="LavenderBlush" />
    <Canvas Name="cvRechts" 
      Margin="10 0 0 0" 
      Width="550" Height="650" 
      Background="LavenderBlush" />
    ...
   StackPanel>
... 
Der Methode StapelChart müssen neben der Canvas, auf die gezeichnet werden soll, noch eine Überschrift sowie die Werte-Bezeichner-Paare übergeben werden. Darüber hinaus gibt es zwölf optionale Eingabemöglichkeiten, die für indivi­du­elle Anpassungen bereitstehen. Die komplette Signatur der Methode sehen Sie in Listing 1. Der Stapel-Chart in Bild 2 zeigt die Einnahmen eines Vereins. Der Aufruf sieht so aus:
Listing 1: Signatur der Methode StapelChart
Public Sub StapelChart(
  cv As Canvas,
  HeadLine As String,
  wbListe As List(Of wbPaar),
  Optional MitSumme As Boolean = True,
  Optional Einheit As String = " Euro",
  Optional nk2 As Boolean = True,
  Optional Farbset As Integer = 0,
  Optional Farbverlauf As Integer = 1,
  Optional sBreite As Integer = 0,
  Optional freeLeft As Integer = 0,
  Optional freeTop As Integer = 0,
  Optional freeBottom As Integer = 0,
  Optional HLFontSize As Integer = 0,
  Optional lines As Boolean = True,
  Optional CU As String = ""
)
  ...
End Sub 
Fiktiver Verein: StapelChart(cvRechts, "Einnahmen",data2, CU:="Herkunft der Gelder im Jahr 2024") (Bild 2) © Autor

StapelChart(cvRechts, "Einnahmen", data2, 
   CU:="Herkunft der Gelder im Jahr 2024") 
Die Grafik wird folglich auf die rechte Canvas gezeichnet (cvRechts), die Überschrift lautet Einnahmen, in data2 liegen die Werte und Bezeichner, und eine optionale Chartunterschrift (CU:=...) wird ebenfalls festgelegt.Weil Wert und Bezeichner quasi unzertrennlich zusammengehören, kennt das Programm die Struktur wbPaar, die wie folgt aussieht:

Structure wbPaar
  Dim w As Decimal ' Wert
  Dim b As String  ' Bezeichner
End Structure 
Da in der Regel mehrere Wert-Bezeichner-Pärchen erforderlich sind, werden diese in einer Liste verwaltet, sie heißt wbListe. Die für den obigen Aufruf zusammengestellte Liste data2, deren Ergebnis Sie in Bild 2 sehen, wird mit folgenden Zeilen erzeugt:

Dim data2 As New List(Of wbPaar) From {
  New wbPaar With {
    .w = 4120.0,  .b = "Mitgliedsbeiträge"},
  New wbPaar With {
    .w = 320.0,   .b = "Zuschüsse"},
  New wbPaar With {
    .w = 640.0,   .b = "Spenden"},
  New wbPaar With {
    .w = 1397.12, .b = "Jubiläumsfest"},
  New wbPaar With {
    .w = 993.8,   .b = "Weihnachtsmarkt"},
  New wbPaar With {
    .w = 44.5,    .b = "Andere"}
} 
Das sieht kompliziert aus, ist es aber nur, weil hier die Daten quasi manuell für eine Beispielgrafik erfasst wurden. Nutzt man StapelChart zur Ausgabe von Grafiken zu den von einem Programm ermittelten Daten, gilt es dafür einmalig den Code für die Übergabe der Daten zu schreiben.

Methode StapelChart

Damit die wenigen Eingaben genügen, um eine ansehnliche Grafik zu ergeben, muss die Methode StapelChart einige Aufgaben erledigen:
  • Prüfen, ob die maximale Anzahl von neun Wert-Bezeichner-Paaren überschritten wurde.
  • Die optionalen Werte passend zur Canvas-Größe einstellen.
  • Die Eckpunkte für die Grafik (StartPt_oben und StartPt_unten) berechnen.
  • Alle Werte in eine Variable von Typ StapelInfos (Listing 2) übernehmen.
Listing 2: Die Struktur StapelInfos
Structure StapelInfos
  Dim cv As Canvas
  Dim Headline As String
  Dim wbListe As List(Of wbPaar)
  Dim nk2 As Boolean
  Dim MitSumme As Boolean
  Dim Einheit As String
  Dim Farbset As Integer
  Dim Farbverlauf As Integer
  Dim sBreite As Integer
  Dim freeLeft As Integer
  Dim freeTop As Integer
  Dim freeBottom As Integer
  Dim HLFontSize As Integer
  Dim CSpaceH As Integer
  Dim StartPt_oben As Point
  Dim StartPt_unten As Point
  Dim Lines As Boolean
  Dim CU As String
End Structure 
  • Die Methode Stapeln(StapelInfos) aufrufen.
Die Übernahme in die Struktur StapelInfos wurde eingebaut, damit der Methode Stapeln alle erforderlichen Daten in einer einzelnen Variablen übergeben werden können. Damit wird es sehr einfach, auf einen Mausklick zu reagieren, Änderungen an den StapelInfos vorzunehmen und die Grafik neu aufzubauen.Das Ermitteln sinnvoller Werte für die optionalen Parameter wurde abhängig von Breite und Höhe der übergebenen Canvas programmiert. Der zugehörige Code-Abschnitt sieht so aus:

Dim SI As New StapelInfos
With SI
  .sBreite = If(sBreite = 0, 
    CInt(cv.Width * 0.325), sBreite)
  .freeLeft = If(freeLeft = 0,
    CInt(cv.Width * 0.125), freeLeft)
  .freeTop = If(freeTop = 0,
    CInt(cv.Height * 0.175), freeTop)
  .freeBottom = If(freeBottom = 0,
    CInt(cv.Height * 0.2), freeBottom)
  .HLFontSize = If(HLFontSize = 0,
    CInt(cv.Height * 0.05), HLFontSize)
End With 
Die Breite der Balken ist mit 32,5 Prozent der Breite der Canvas spezifiziert, die Größe der Überschrift beträgt 5 Prozent der Höhe der Canvas.Sagen Ihnen die hier gewählten Proportionen nicht zu, können Sie diese einfach ändern und ausprobieren. Nicht ­alle
automatisch gesetzten Größen sind hier zu finden. Die Größe der Schrift der Bezeichner neben den einzelnen Blöcken wird erst in Stapeln(StapelInfos) mit 60 Prozent der Größe der Überschrift festgelegt, und die Schriftgröße der Chart-Unterschrift wird dort mit der Schriftgröße der Bezeichner gleichgesetzt.Zudem wird die Position der optionalen Linien erst in der Methode Stapeln ermittelt. Den kompletten Code von StapelChart, der mit dem Aufruf von Stapeln(SI) endet, finden Sie in Listing 3.
Listing 3: Die Methode StapelChart
Public Sub StapelChart(cv As Canvas, 
  HeadLine As String,
  wbListe As List(Of wbPaar),
  Optional ... siehe Listing 1)
  ' Hier werden die Daten in die Struktur 
  ' StapelInfos gepackt, die optimalen Werte
  ' passend zur Canvas-Größe eingestellt
  ' und an Stapeln(StapelInfos) weitergereicht.
  If wbListe.Count > 9 Then
    MsgBox("Maximal 9 Wertepaare ... !")
    Exit Sub
  End If
  Dim SI As New StapelInfos
  With SI
    .cv = cv
    .wbListe = wbListe
    .Einheit = Einheit
    .nk2 = nk2
    .MitSumme = MitSumme
    .Farbset = Farbset
    .Headline = HeadLine
    .Farbverlauf = Farbverlauf
    .Lines = lines
    .CU = CU
    ' Optionale Werte einstellen
    .sBreite = If(sBreite = 0, 
      CInt(cv.Width * 0.325), sBreite)
    .freeLeft = If(freeLeft = 0, 
      CInt(cv.Width * 0.125), freeLeft)
    .freeTop = If(freeTop = 0, 
      CInt(cv.Height * 0.175), freeTop)
    .freeBottom = If(freeBottom = 0, 
      CInt(cv.Height * 0.2), freeBottom)
    .HLFontSize = If(HLFontSize = 0, 
      CInt(cv.Height * 0.05), HLFontSize)
    'Startpunkte oben / unten berechnen
    .StartPt_oben = New Point(
      .freeLeft, .freeTop)
    .StartPt_unten = New Point(
      .freeLeft, cv.Height - .freeBottom)
  End With
  Stapeln(SI)
End Sub 

Die Methode Stapeln

Die Methode Stapeln übernimmt alle StapelInfos und kann daraus den Stapel-Chart aufbauen. Folgende Teilaufgaben werden hier erledigt:Die Canvas wird leer geräumt, damit nicht mehrere Stapel übereinander gedruckt werden. Falls gewünscht, werden Linien gezeichnet, danach die Daten zur Headline in der Struktur TBDetails(Listing 4) zusammengefasst und deren Init-Methode aufgerufen, damit sie mit der Methode showOnCanvas auf der Leinwand platziert werden kann:
Listing 4: Die Strukturen TBDetails und TextWH
Public Structure TBDetails
  Dim Text As String
  Dim uiObj As TextBlock
  Dim twh As TextWH
  Dim Start As Point
  Dim FontSize As Integer
  Dim FontWeight As FontWeight
  Dim ForeGround As Brush
  Dim MarginBottom As Integer
  Sub Init()
    FontSize = Math.Max(FontSize, 14)
    ForeGround = If(ForeGround, Brushes.Black)
    MarginBottom = Math.Max(MarginBottom, 4)
    uiObj = getTextBlock(Me)
    twh = TextHöheUndBreite(uiObj,
      VisualTreeHelper.GetDpi(
      Application.Current.MainWindow)
      .PixelsPerDip)
  End Sub
End Structure
Public Structure TextWH
  Dim width As Double
  Dim height As Double
End Structure 

Sub Stapeln (SI as StapelInfos)
  ...
  Dim HL As New TBDetails
  With HL
    .Text = SI.Headline
    .FontSize = SI.HLFontSize
    .FontWeight = FontWeights.SemiBold
    .Init()
    .Start.X = SI.StartPt_oben.X
    .Start.Y = SI.HLFontSize \ 2
  End With
  showOnCanvas(SI.cv, HL) 
  ...
End Sub
Sub showOnCanvas(c As Canvas, obj As Object)
  Canvas.SetLeft(obj.uiObj, obj.start.X)
  Canvas.SetTop(obj.uiObj, obj.start.Y)
  c.Children.Add(obj.uiObj)
End Sub  
Nachdem die Headline gesetzt ist, wird ermittelt, wie viel Platz für die eigentliche Grafik zur Verfügung steht:

SI.CSpaceH = CInt(
  SI.cv.Height - SI.freeTop - SI.freeBottom) 
Dann wird die Summe der Werte errechnet, um die Pixel pro Werteinheit auszurechnen:

Dim SumWerte As Decimal = 
  SI.wbListe.Sum(Function(item) item.w)
Dim yPixelProWerteinheit As Double = 
  SI.CSpaceH / SumWerte  
Falls gewünscht, wird die Summe der einzelnen Werte ebenfalls in TBDetails gekleidet und per showOnCanvas angezeigt:

...
If SI.MitSumme Then
  Dim SumHL As New TBDetails
  With SumHL
    .Text = If(SI.nk2, SumWerte.nk2 + SI.Einheit, 
               SumWerte.ToString + SI.Einheit)
    .Text = SumWerte.nk2 + SI.Einheit
    .FontSize = CInt(SI.HLFontSize * 0.6)
    .FontWeight = FontWeights.SemiBold
    .Init()
    .Start.X = SI.StartPt_oben.X + 
      (SI.sBreite - .twh.width) \ 2
    .Start.Y = SI.StartPt_oben.Y - 
      (.FontSize + .FontSize \ 4)
  End With
  showOnCanvas(SI.cv, SumHL)
End If
... 
Dann ist es so weit und die Blöcke werden gezeichnet, siehe Listing 5. Die Rechtecke werden von unten nach oben gestapelt. Für jeden Wert gibt es einen eigenen Block mit Bezeichnung. Die Breite des Blocks bestimmt SI.sBreite, die Höhe wird mit wert * yPixelProWerteinheit errechnet. Als Mindesthöhe – damit man den Block noch gut erkennt – sind 5 Pixel vorgegeben. Gezeichnet werden die Blöcke von der Methode blRechteck, welcher die Farbe mit der Farbnummer f aus dem gewählten Farbset (im Beispielprogramm gibt es drei Farbsets) übergeben wird:
Listing 5: Blöcke zeichnen
Sub Stapeln(SI as StapelInfos)
  ...
  Dim start As Point = SI.StartPt_unten
  Dim f As Integer = 0 ' Farbzähler
  Dim BU As New TBDetails
  For Each item In SI.wbListe
    Dim höhe As Double = Math.Max(
      item.w * yPixelProWerteinheit, 5)
    ' neuer Startpunkt: X bleibt gleich, 
    ' Y muss vermindert werden
    start.Y = start.Y - höhe
    ' Das Rechteck zeichnen 
    blRechteck(SI, start, höhe, 
      blFarbe(f, SI.Farbset))
    
    With BU  ' Die Beschriftung einfügen
      ' Decimal.nk2 = 2 Nachkommastellen plus 
      ' Umwandeln in einen String
      ' Für 2-zeilige Texte muss das Rechteck 
      ' mindestens 16 Prozent des Stapels ein-
      ' einnehmen, sonst bleibt es bei einer Zeile
      If item.w > 0.16 * SumWerte Then
        .Text = If(SI.nk2, item.b + vbCrLf + 
          item.w.nk2 + SI.Einheit, item.b + 
          vbCrLf + item.w.ToString + SI.Einheit)
      Else
        .Text = If(SI.nk2, item.b + ": " + 
          item.w.nk2 + SI.Einheit, item.b + 
          ": " + item.w.ToString + SI.Einheit)
      End If
      .FontSize = CInt(SI.HLFontSize * 0.6)
      .FontWeight = FontWeights.SemiBold
      .ForeGround = New SolidColorBrush(blFarbe(
        f, SI.Farbset))
      .Init()
      .Start.X = start.X + SI.sBreite + 8 '
      .Start.Y = start.Y + höhe / 2 -
        .twh.height / 3 
    End With
    showOnCanvas(SI.cv, BU)
    f += 1 ' die nächste Farbe ist dran
  Next  
  ...
End Sub 

blRechteck(SI, start, höhe, 
  blFarbe(f, SI.Farbset)) 
Eine Besonderheit bei der Beschriftung (BU) ist, dass für Werte ab 16 Prozent der Gesamtsumme zweizeilige Bezeichner vergeben werden, kleinere Werte sind nur einzeilig. Als FontSize werden 60 Prozent der Größe der Überschrift eingestellt. showOnCanvas übernimmt wieder das Platzieren der BUs auf der Leinwand. Die letzte Aufgabe der Methode Stapeln ist das Setzen der Chart-Unterschrift mit der Methode CU_auf_Canvas_setzen, die auch schon für die vbDonuts [1] für diesen Zweck genutzt wurde. Weil WPF die Zeilenabstände bei zweizeiligen BUs für meinen Geschmack zu groß wählt, wurden diese vermindert mit:

TextBlock.LineHeight = TextBlock.FontSize 
TextBlock.LineStackingStrategy = 
  LineStackingStrategy.BlockLineHeight 
Achtung: Fehlt die Zeile .LineStackingStrategy …, wird der Eintrag für LineHeight nicht berücksichtigt.

Blöcke zeichnen

Das Zeichnen der Blöcke erledigt die Methode blRechteck mit wenigen Zeilen Code:

Public Sub blRechteck(SI As StapelInfos, 
    Start As Point, h As Double, farbe As Color)
  Dim myRect As New Rectangle() With {
    .Width = SI.sBreite,
    .Height = h,
    .Stroke = Brushes.White,
    .StrokeThickness = 1
  }
  ... 
  ' Setze die Position des Rechtecks
  Canvas.SetLeft(myRect, Start.X)
  Canvas.SetTop(myRect, Start.Y)
  SI.cv.Children.Add(myRect)
End Sub 
Anstelle der drei Punkte im Code-Schnipsel oben wird die Füllfarbe des Rechtecks gesetzt, wobei das Beispielprogramm neben einer SolidColorBrush-Variante zwei unterschiedliche Farbverläufe vorsieht:

  Select Case SI.Farbverlauf
    Case 1
      myRect.Fill = 
        Farbverlauf_V1(farbe)
    Case 2
      myRect.Fill = Farbverlauf_Metallic(farbe)
    Case Else
      myRect.Fill = New SolidColorBrush(farbe)
  End Select 
Außerdem erhält das Rechteck drei Eventhandler, sodass es auf Klicks mit der linken und rechten Maustaste, auf das Drehen des Mausrads sowie auf Doppelklicks mit der linken Maustaste reagieren kann. Die Interaktionen des Anwenders sollen folgende Auswirkungen haben:
  • Einfacher Klick mit der linken Maustaste: Sortieren der Werte aufsteigend und beim nächsten Klick absteigend, siehe Bild 3.
Links die ursprünglichen Werte, rechts die per Mausklick sortierten Werte. Ein weiterer Linksklick sortiert die Blöcke in umgekehrter Reihenfolge (Bild 3) © Autor
  • Doppelklick mit der linken Maustaste: Zusammenfassen aller Werte, die einzeln kleiner als 5 Prozent der Gesamtsumme sind, zum neuen Wert Übrige, siehe Bild 4 und Listing 6.
Ein großer, viele Mini-Werte: Ein Doppelklick links fasst die kleinen Werte zu „Übrige“ zusammen (Bild 4) © Autor
Listing 6: Zusammenfassen kleiner Werte
Sub übrigeZusammenfassen(ByRef wb As List(
    Of wbPaar), Optional Prozent As Integer= 5)
  Dim sum As Decimal = 
    wb.Sum(Function(item) item.w)
  ' Anhand des übergebenen Parameters Prozent
  ' den Schwellenwert min berechnen
  Dim min As Decimal = sum * Prozent / 100
  ' MiniWerte herausfiltern
  Dim MiniWerte As Integer = wb.Where(
    Function(item) item.w < min).Count()
  If MiniWerte < 2 Then Exit Sub 
  Dim übrig As Decimal
  ' Summiere Werte kleiner als min und 
  ' lösche die entsprechenden Elemente
  übrig = wb.Where(
    Function(item) item.w < min).Sum(
    Function(item) item.w)
  wb.RemoveAll(
    Function(item) item.w < min)
  wb.Add(New wbPaar With {
    .w = übrig, .b = "Übrige"})
End Sub 
  • Einfacher Rechtsklick: Zufälliges Neuanordnen der Reihenfolge der Blöcke, damit zu eng beieinanderstehende Beschriftungen sich voneinander entfernen (Bild 5).
Ungünstige Reihenfolge (links): Ein einfacher Rechtsklick ändert die Abfolge der Daten (Bild 5) © Autor
  • Mausrad nach oben drehen: Farbverlauf wechseln; nach unten drehen: Farbset wechseln (Bild 6).
Die Farbsets und -verläufe werden per Mausrad geändert (Bild 6) © Autor
Da Rectangle-Objekte eigentlich kein Doppelklick-Ereignis kennen, wird dieser Handler hier vorgestellt. Nach der Zuordnung des Farbverlaufs wird der Handler hinzugefügt, wobei ihm auch die StapelInfos (SI) übergeben werden:

AddHandler myRect.MouseLeftButtonDown, 
  Sub(sender As Object, e As MouseButtonEventArgs)
    myRect_MouseLeftButtonDown(sender, e, SI)
  End Sub 
Der Eventhandler sieht wie folgt aus:

Private Sub myRect_MouseLeftButtonDown(
    sender As Object, e As MouseButtonEventArgs, 
    SI As StapelInfos)
  If e.ClickCount = 2 Then
    übrigeZusammenfassen(SI.wbListe)
  Else
    If SI.wbListe.Item(1).w < 
      SI.wbListe.Item(0).w Then
        SI.wbListe = 
          SI.wbListe.OrderBy(
          Function(item) item.w)
          .ToList()
    Else
      SI.wbListe = SI.wbListe
        .OrderByDescending(
        Function(item) item.w)
        .ToList()
    End If
  End If
  Stapeln(SI)
End Sub 
Steht der Klickzähler auf 2, wird die mit den StapelInfos übergebene wbListe auf- beziehungsweise absteigend sortiert, die StapelInfos werden angepasst und mit Stapeln(SI) das Neuzeichnen des Charts veranlasst. Die beiden übrigen Eventhandler sind ähnlich, wichtigster Unterschied: Anstelle der MouseButtonEventArgs müssen dem Mausrad-Handler die MouseWheelEventArgs übergeben werden.

Fussnoten

  1. Bernhard Lauer, Donut im Eigenbau, dotnetpro 4-5/2025, Seite 54 ff., http://www.dotnetpro.de/A2504-05VBDonuts

Neueste Beiträge

KI lässt Entwickler ihre Leidenschaft zum Programmieren neu entdecken - Motivation
Softwareentwicklung ist gleich Spaßfreie Zone? Das muss nicht sein: Der Beitrag beleuchtet, wie Teams ihren Kopf wieder freibekommen und ihre Freude am Entwickeln neu entdecken.
5 Minuten
10. Jul 2025
Spotlight #1: Azure IoT Operations, Video Teil 2/3 - DWX Spotlight
Das erste DWX Spotlight mit Special Guest Florian Bader. Im Teil 2 des Videos erklärt Florian unter anderem, wie es mit der Sicherheit bei Industrieanlagen und Messdatenerfassung aussieht.
2 Minuten
10. Jul 2025
Contacts guaranteed: Die Partnerunternehmen auf der DWX 2025 - Konferenz
Über 30 führende Unternehmen, unzählige Impulse: Auf der DWX 2025 in Mannheim zeigten unsere Partner, was Tech heute kann – und morgen möglich macht.
2 Minuten
10. Jul 2025
Miscellaneous

Das könnte Dich auch interessieren

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
Dateikompression mit paralleler Verarbeitung
Ein Tool zur parallelen Dateikompression in Go kombiniert Geschwindigkeit und Speicherverwaltung durch ein intelligentes Semaphore-System.
2 Minuten
23. Jun 2025
ZLinq: LINQ-Bibliothek ohne Speicherallokation
Die neu veröffentlichte ZLinq von Yoshifumi Kawai verspricht eine effiziente LINQ-Bibliothek ohne Speicherallokationen.
3 Minuten
17. Jun 2025
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige