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:

...
  <span class="hljs-tag">&lt;<span class="hljs-name">StackPanel</span> <span class="hljs-attr">Orientation</span>=<span class="hljs-string">"Horizontal"</span> </span>
<span class="hljs-tag">      <span class="hljs-attr">Margin</span>=<span class="hljs-string">"30"</span> <span class="hljs-attr">Height</span>=<span class="hljs-string">"550"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Canvas</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"cvLinks"</span> <span class="hljs-attr">Margin</span>=<span class="hljs-string">"10 0 0 0"</span> </span>
<span class="hljs-tag">      <span class="hljs-attr">Width</span>=<span class="hljs-string">"550"</span> <span class="hljs-attr">Height</span>=<span class="hljs-string">"650"</span></span>
<span class="hljs-tag">      <span class="hljs-attr">Background</span>=<span class="hljs-string">"LavenderBlush"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Canvas</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"cvRechts"</span> </span>
<span class="hljs-tag">      <span class="hljs-attr">Margin</span>=<span class="hljs-string">"10 0 0 0"</span> </span>
<span class="hljs-tag">      <span class="hljs-attr">Width</span>=<span class="hljs-string">"550"</span> <span class="hljs-attr">Height</span>=<span class="hljs-string">"650"</span> </span>
<span class="hljs-tag">      <span class="hljs-attr">Background</span>=<span class="hljs-string">"LavenderBlush"</span> /&gt;</span>
    ...
   <span class="hljs-tag">&lt;/<span class="hljs-name">StackPanel</span>&gt;</span>
... 
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
&lt;span class="hljs-keyword"&gt;Public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;Sub&lt;/span&gt; StapelChart(&lt;br/&gt;  cv &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; Canvas,&lt;br/&gt;  HeadLine &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;String&lt;/span&gt;,&lt;br/&gt;  wbListe &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; List(&lt;span class="hljs-keyword"&gt;Of&lt;/span&gt; wbPaar),&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; MitSumme &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt; = &lt;span class="hljs-literal"&gt;True&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; Einheit &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;String&lt;/span&gt; = &lt;span class="hljs-string"&gt;" Euro"&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; nk2 &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt; = &lt;span class="hljs-literal"&gt;True&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; Farbset &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; Farbverlauf &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;1&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; sBreite &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; freeLeft &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; freeTop &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; freeBottom &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; HLFontSize &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt; = &lt;span class="hljs-number"&gt;0&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; lines &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt; = &lt;span class="hljs-literal"&gt;True&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Optional&lt;/span&gt; CU &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;String&lt;/span&gt; = &lt;span class="hljs-string"&gt;""&lt;/span&gt;&lt;br/&gt;)&lt;br/&gt;  ...&lt;br/&gt;&lt;span class="hljs-keyword"&gt;End&lt;/span&gt; &lt;span class="hljs-keyword"&gt;Sub&lt;/span&gt; 
Fiktiver Verein: StapelChart(cvRechts, "Einnahmen",data2, CU:="Herkunft der Gelder im Jahr 2024") (Bild 2) © Autor

StapelChart(<span class="hljs-name">cvRechts</span>, <span class="hljs-string">"Einnahmen"</span>, data2, 
   CU<span class="hljs-symbol">:=</span><span class="hljs-string">"Herkunft der Gelder im Jahr 2024"</span>) 
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:

<span class="hljs-keyword">Structure</span> wbPaar
  <span class="hljs-keyword">Dim</span> w <span class="hljs-keyword">As</span> Decimal ' Wert
  <span class="hljs-keyword">Dim</span> b <span class="hljs-keyword">As</span> String  ' Bezeichner
End <span class="hljs-keyword">Structure</span> 
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 {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">4120.0</span>,  <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Mitgliedsbeiträge"</span>},
  New wbPaar With {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">320.0</span>,   <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Zuschüsse"</span>},
  New wbPaar With {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">640.0</span>,   <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Spenden"</span>},
  New wbPaar With {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">1397.12</span>, <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Jubiläumsfest"</span>},
  New wbPaar With {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">993.8</span>,   <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Weihnachtsmarkt"</span>},
  New wbPaar With {
    <span class="hljs-selector-class">.w</span> = <span class="hljs-number">44.5</span>,    <span class="hljs-selector-class">.b</span> = <span class="hljs-string">"Andere"</span>}
} 
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
&lt;span class="hljs-keyword"&gt;Structure&lt;/span&gt; StapelInfos&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; cv &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; Canvas&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; Headline &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;String&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; wbListe &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; List(&lt;span class="hljs-keyword"&gt;Of&lt;/span&gt; wbPaar)&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; nk2 &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; MitSumme &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; Einheit &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;String&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; Farbset &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; Farbverlauf &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; sBreite &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; freeLeft &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; freeTop &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; freeBottom &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; HLFontSize &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; CSpaceH &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Integer&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; StartPt_oben &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; Point&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; StartPt_unten &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; Point&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; Lines &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Boolean&lt;/span&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;Dim&lt;/span&gt; CU &lt;span class="hljs-keyword"&gt;As&lt;/span&gt; String&lt;br/&gt;End &lt;span class="hljs-keyword"&gt;Structure&lt;/span&gt; 
  • 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, &lt;br/&gt;  HeadLine As String,&lt;br/&gt;  wbListe As List(Of wbPaar),&lt;br/&gt;  Optional ... siehe Listing 1)&lt;br/&gt;  ' Hier werden die Daten in die Struktur &lt;br/&gt;  ' StapelInfos gepackt, die optimalen Werte&lt;br/&gt;  ' passend zur Canvas-Größe eingestellt&lt;br/&gt;  ' und an Stapeln(StapelInfos) weitergereicht.&lt;br/&gt;  If wbListe.Count &amp;gt; 9 Then&lt;br/&gt;    MsgBox("Maximal 9 Wertepaare ... !")&lt;br/&gt;    Exit Sub&lt;br/&gt;  End If&lt;br/&gt;  Dim SI As New StapelInfos&lt;br/&gt;  With SI&lt;br/&gt;    .cv = cv&lt;br/&gt;    .wbListe = wbListe&lt;br/&gt;    .Einheit = Einheit&lt;br/&gt;    .nk2 = nk2&lt;br/&gt;    .MitSumme = MitSumme&lt;br/&gt;    .Farbset = Farbset&lt;br/&gt;    .Headline = HeadLine&lt;br/&gt;    .Farbverlauf = Farbverlauf&lt;br/&gt;    .Lines = lines&lt;br/&gt;    .CU = CU&lt;br/&gt;    ' Optionale Werte einstellen&lt;br/&gt;    .sBreite = If(sBreite = 0, &lt;br/&gt;      CInt(cv.Width * 0.325), sBreite)&lt;br/&gt;    .freeLeft = If(freeLeft = 0, &lt;br/&gt;      CInt(cv.Width * 0.125), freeLeft)&lt;br/&gt;    .freeTop = If(freeTop = 0, &lt;br/&gt;      CInt(cv.Height * 0.175), freeTop)&lt;br/&gt;    .freeBottom = If(freeBottom = 0, &lt;br/&gt;      CInt(cv.Height * 0.2), freeBottom)&lt;br/&gt;    .HLFontSize = If(HLFontSize = 0, &lt;br/&gt;      CInt(cv.Height * 0.05), HLFontSize)&lt;br/&gt;    'Startpunkte oben / unten berechnen&lt;br/&gt;    .StartPt_oben = New Point(&lt;br/&gt;      .freeLeft, .freeTop)&lt;br/&gt;    .StartPt_unten = New Point(&lt;br/&gt;      .freeLeft, cv.Height - .freeBottom)&lt;br/&gt;  End With&lt;br/&gt;  Stapeln(SI)&lt;br/&gt;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&lt;br/&gt;  Dim Text As String&lt;br/&gt;  Dim uiObj As TextBlock&lt;br/&gt;  Dim twh As TextWH&lt;br/&gt;  Dim Start As Point&lt;br/&gt;  Dim FontSize As Integer&lt;br/&gt;  Dim FontWeight As FontWeight&lt;br/&gt;  Dim ForeGround As Brush&lt;br/&gt;  Dim MarginBottom As Integer&lt;br/&gt;  Sub Init()&lt;br/&gt;    FontSize = Math.Max(FontSize, 14)&lt;br/&gt;    ForeGround = If(ForeGround, Brushes.Black)&lt;br/&gt;    MarginBottom = Math.Max(MarginBottom, 4)&lt;br/&gt;    uiObj = getTextBlock(Me)&lt;br/&gt;    twh = TextHöheUndBreite(uiObj,&lt;br/&gt;      VisualTreeHelper.GetDpi(&lt;br/&gt;      Application.Current.MainWindow)&lt;br/&gt;      .PixelsPerDip)&lt;br/&gt;  End Sub&lt;br/&gt;End Structure&lt;br/&gt;Public Structure TextWH&lt;br/&gt;  Dim width As Double&lt;br/&gt;  Dim height As Double&lt;br/&gt;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)&lt;br/&gt;  ...&lt;br/&gt;  Dim start As Point = SI.StartPt_unten&lt;br/&gt;  Dim f As Integer = 0 ' Farbzähler&lt;br/&gt;  Dim BU As New TBDetails&lt;br/&gt;  For Each item In SI.wbListe&lt;br/&gt;    Dim höhe As Double = Math.Max(&lt;br/&gt;      item.w * yPixelProWerteinheit, 5)&lt;br/&gt;    ' neuer Startpunkt: X bleibt gleich, &lt;br/&gt;    ' Y muss vermindert werden&lt;br/&gt;    start.Y = start.Y - höhe&lt;br/&gt;    ' Das Rechteck zeichnen &lt;br/&gt;    blRechteck(SI, start, höhe, &lt;br/&gt;      blFarbe(f, SI.Farbset))&lt;br/&gt;    &lt;br/&gt;    With BU  ' Die Beschriftung einfügen&lt;br/&gt;      ' Decimal.nk2 = 2 Nachkommastellen plus &lt;br/&gt;      ' Umwandeln in einen String&lt;br/&gt;      ' Für 2-zeilige Texte muss das Rechteck &lt;br/&gt;      ' mindestens 16 Prozent des Stapels ein-&lt;br/&gt;      ' einnehmen, sonst bleibt es bei einer Zeile&lt;br/&gt;      If item.w &amp;gt; 0.16 * SumWerte Then&lt;br/&gt;        .Text = If(SI.nk2, item.b + vbCrLf + &lt;br/&gt;          item.w.nk2 + SI.Einheit, item.b + &lt;br/&gt;          vbCrLf + item.w.ToString + SI.Einheit)&lt;br/&gt;      Else&lt;br/&gt;        .Text = If(SI.nk2, item.b + ": " + &lt;br/&gt;          item.w.nk2 + SI.Einheit, item.b + &lt;br/&gt;          ": " + item.w.ToString + SI.Einheit)&lt;br/&gt;      End If&lt;br/&gt;      .FontSize = CInt(SI.HLFontSize * 0.6)&lt;br/&gt;      .FontWeight = FontWeights.SemiBold&lt;br/&gt;      .ForeGround = New SolidColorBrush(blFarbe(&lt;br/&gt;        f, SI.Farbset))&lt;br/&gt;      .Init()&lt;br/&gt;      .Start.X = start.X + SI.sBreite + 8 '&lt;br/&gt;      .Start.Y = start.Y + höhe / 2 -&lt;br/&gt;        .twh.height / 3 &lt;br/&gt;    End With&lt;br/&gt;    showOnCanvas(SI.cv, BU)&lt;br/&gt;    f += 1 ' die nächste Farbe ist dran&lt;br/&gt;  Next  &lt;br/&gt;  ...&lt;br/&gt;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(&lt;br/&gt;    Of wbPaar), Optional Prozent As Integer= 5)&lt;br/&gt;  Dim sum As Decimal = &lt;br/&gt;    wb.Sum(Function(item) item.w)&lt;br/&gt;  ' Anhand des übergebenen Parameters Prozent&lt;br/&gt;  ' den Schwellenwert min berechnen&lt;br/&gt;  Dim min As Decimal = sum * Prozent / 100&lt;br/&gt;  ' MiniWerte herausfiltern&lt;br/&gt;  Dim MiniWerte As Integer = wb.Where(&lt;br/&gt;    Function(item) item.w &amp;lt; min).Count()&lt;br/&gt;  If MiniWerte &amp;lt; 2 Then Exit Sub &lt;br/&gt;  Dim übrig As Decimal&lt;br/&gt;  ' Summiere Werte kleiner als min und &lt;br/&gt;  ' lösche die entsprechenden Elemente&lt;br/&gt;  übrig = wb.Where(&lt;br/&gt;    Function(item) item.w &amp;lt; min).Sum(&lt;br/&gt;    Function(item) item.w)&lt;br/&gt;  wb.RemoveAll(&lt;br/&gt;    Function(item) item.w &amp;lt; min)&lt;br/&gt;  wb.Add(New wbPaar With {&lt;br/&gt;    .w = übrig, .b = "Übrige"})&lt;br/&gt;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 &lt; 
      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

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
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Bausteine guter Architektur - Entwurf und Entwicklung wartbarer Softwaresysteme, Teil 2
Code sauberer gestalten anhand von wenigen Patterns und Grundhaltungen.
6 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige