Chart Component Blazor

Nachdem ich über Blazor und die parallelen zum MVVM Model geschrieben habe, will ich mir einen weiteren Blog Artikel von mir vornehmen. Dabei wurde eine Chart UI Element erstellt unter Zuhilfenahme von reichlich JavaScript. Mit dem neuen Blazor Know How gehts nun an einen rewrite der Blazor Component ganz ohne JavaScript. Ich will vorab anmerken, das es nicht weniger coding Aufwand ist.

Die wiederverwendbare Blazor Komponente soll ein Kuchendiagramm darstellen. Als vektorbasiertes Zeichenformat bietet sich SVG an, das man ins HTML Dokument einbetten kann und dann auch Bestandteil des DOM wird. Leider kann man mit SVG Kreissegmente (arc) zeichnen sondern nur Pfade (path).

Folgender SVG Code wird dann zu einem sehr einfachen Pie Chart.

   1:  <svg viewBox="0 0 200  200">
   2:  <a href="https://localhost:44321/usechart#0">
   3:  <path d="M100,100 L100,0 A100,100 1 0 1 115.9,1.3  z" transform="rotate(0.0, 100, 100)" fill="red"></path>
   4:  </a><a href="https://localhost:44321/usechart#1"><path d="M100,100 L100,0 A100,100 1 1 1 95.2,199.9  z" transform="rotate(9.1, 100, 100)" fill="aqua"></path></a>
   5:  <a href="https://localhost:44321/usechart#2">
   6:  <path d="M100,100 L100,0 A100,100 1 0 1 151.6,14.3  z" transform="rotate(191.9, 100, 100)" fill="fuchsia"></path></a>
   7:  <a href="https://localhost:44321/usechart#3">
   8:  <path d="M100,100 L100,0 A100,100 1 0 1 171.6,30.1  z" transform="rotate(222.9, 100, 100)" fill="green"></path></a>
   9:  <a href="https://localhost:44321/usechart#4">
  10:  <path d="M100,100 L100,0 A100,100 1 0 1 200.0,102.4  z" transform="rotate(268.6, 100, 100)" fill="blue">
  11:  </path>
  12:  </a>
  13:  </svg>

image

Der Artikel kann nicht auf die Details von SVG eingehen. Das Kuchenstück wird von einem Zentrum (M100,100) gezeichnet. Es folgt eine Linie (L100,0). Komplex sind die Endpunkte zu berechnen, beginnend von A und Endent mit  X Y Koordinaten (151.6,14.3) errechnet mit Winkelfunktionen sinus und cosinus. Dann wird das Stück um dem Winkel gedreht und ein Hyperlink außen rum gelegt um die Stücke selektieren zu können. Die CSharp Logik folgt im späteren Code.

Blazor Component und Rendertree

Eine Blazor Komponente rein als C# Klasse chart.cs, benötigt als Basisklasse ComponentBase.  Dies ist kein muss für Razor Components. Häufig werden Komponenten als .razor Dateien angelegt. Dann mit einem Mix aus HTML und @Code. Da diese Chart Komponente aber nur reinen SVG Code rendern soll, bietet sich die pur Code Lösung an.

Der wichtigste Part ist das bauen des Trees an SVG Elementen wie oben im Code dargestellt. Wie in HTML üblich wird ein Element erstellt (openelement). Das wiederrum Unterlemente behinhalten kann. Geschlossen wird per CloseElement, das zuletzt geöffnete. Für uns wichtig sind die Attribute des path Elements, die per AddAttribute hinzugefügt werden.

   1:  protected override void BuildRenderTree(RenderTreeBuilder builder)
   2:  {
   3:     var seq = 0;
   4:     double radius = 100;
   5:     double winkel;
   6:     double deltawinkel = 0;
   7:     double summe = 360 / Daten.Sum();
   8:     builder.OpenElement(seq, "figure");
   9:     builder.OpenElement(++seq, "div");
  10:     builder.OpenElement(++seq, "svg");
  11:     builder.AddAttribute(++seq, "viewBox", "0 0 200  200");
  12:     for (int i = 0; i < Daten.Count(); i++)
  13:     {
  14:        builder.OpenElement(++seq, "a");
  15:        builder.AddAttribute(++seq, "href", $"{ NavigationManager.Uri}#{i.ToString()}");
  16:        winkel = Daten[i] * summe;
  17:        var winkelInRadians = (winkel - 90) * Math.PI / 180.0;
  18:        builder.OpenElement(++seq, "path");
  19:        builder.AddAttribute(++seq, "d", $"M{radius},{radius} L{radius}," +
  20:            $"0 A{radius},{radius} 1 {(winkel > 180 ? 1 : 0)} 1 " +
  21:            $"{(radius + radius * Math.Cos(winkelInRadians)).ToString("0.0", CultureInfo.InvariantCulture)}," +
  22:            $"{(radius + radius * Math.Sin(winkelInRadians)).ToString("0.0", CultureInfo.InvariantCulture)}  z");
  23:        builder.AddAttribute(++seq, "transform", $"rotate({(deltawinkel).ToString("0.0", CultureInfo.InvariantCulture)}, {radius}, {radius})");
  24:        //builder.AddAttribute(++seq, "stroke", "black");
  25:        builder.AddAttribute(++seq, "fill", Farben[i]);
  26:        builder.CloseElement();
  27:        builder.CloseElement();
  28:        deltawinkel += winkel;
  29:       }
  30:  builder.CloseElement();
  31:  builder.CloseElement();
  32:  builder.CloseElement();
  33:  }

Kurz noch Exkurs zur Methode der Segmentauswahl durch den Benutzer. Hier wird eine Browsernavigation mit einem Anchor Tag genutzt, die SVG grundsätzlich unterstützt. Es wäre auch möglich die SVG Elemente per JavaScript onclick mit einer Methode zu verdrahten. Normalerweise bekommt das Blazor hin. Deklarativ im HTML code mit @onclick=.  Oder im Rendertree mit   builder.AddAttribute(++seq, "onclick", onclick); Letzteres kompiliert bei mir nicht für SVG Elemente. Also blieb mir nur der Weg über die Page Navigation. Hierzu bietet Blazor einen Navigation Manager der per DI injiziert werden muss. Folgender Code wird in der Chart.cs eingefügt.

   1:  [Inject]
   2:  protected NavigationManager NavigationManager { get; set; }
   3:   
   4:  public String[] Farben { get; set; } = { "red", "aqua", "fuchsia", "green", "blue", "lime", "yellow", "purple", "silver" };
   5:  private double[] _Daten;
   6:  public double[] Daten
   7:   {
   8:    get { return _Daten; }
   9:    set       {
  10:         _Daten = value;
  11:         this.StateHasChanged();
  12:         }
  13:  }

Das Array an Farben sozusagen auf Vorrat und die Daten werden später extern beim Aufruf und Nutzung der Chart Component mitgegeben.

Navigation Manager

Der Navigation Manager beinhaltet eine Reihe von Hilfsfunktionen rund um Historie und Routing. Da als Singleton immer vorhanden holt man sich eine Referenz per Dependency Injection. Wir wollen wissen, wann der Benutzer ein Segment ausgewählt hat und so die Browser Navigation per a href auslöst. Man kann sich in das page Changed Event, das eigentlich OnInit heisst, reinhängen und LocationChanged abonnieren.

   1:  protected override void OnInitialized()
   2:     {
   3:         NavigationManager.LocationChanged += LocationChanged;
   4:         base.OnInitialized();
   5:  }

Klugerweise im Dispose auch wieder das Abo kündigen.

Nun erhält meine Chart.cs ein eigenes Event auf das wiederum chart Konsumenten ein Abo abschließen können. Code in .NET schon seit langem Action Parameter bzw Event. Da wir den gewählten Segment Namen oder ID übergeben müssen mit Action <string>. Im LocationChanged wird dann das Event per Invoke ausgelöst.

   1:  public event Action<string> OnItemSelected;
   2:  private void LocationChanged(object sender, LocationChangedEventArgs e)
   3:   {
   4:       Uri u = new Uri(NavigationManager.Uri);
   5:       OnItemSelected?.Invoke(u.Fragment);
   6:   }

 

Hinweis: Blazor kann bei Änderungen im DOM automatisch das UI neu rendern. In diesem Fall bekommt Blazor es aber nicht mit wenn sich die Chart Daten ändern. Deswegen im Setter Zeile 11 die Methode StateHasChanged() aufgerufen werden muss.

Chart Komponente konsumieren

Das Visual Studio Blazor Projekt erhält nun eine Razor Komponente. Also eine Datei mit der Endung .razor. Wegen unserem gewählten Weg über die URL muss für das Routing Zeile 1 hinzugefügt werden. Um den Fall des leeren Parameters in der Url zu behandeln Zeile 2. Das Chart Objekt wird wie ein HTML Element eingefügt.

image

Das @ref bewirkt, das auch später im Code eine Referenz auf das Objekt hergestellt werden kann. Rein zu Demo Zwecken dient der Button samt Methode um die Daten des Charts zu wechseln.

   1:  @page "/usechart/{Id}"
   2:  @page "/usechart"
   3:  <h3>UseChart</h3>
   4:  <div class="row">
   5:      <div class="col-md-4">
   6:          <div style="height:200px; width:200px; background:#010000">
   7:              <Chart @ref="chart1"></Chart>
   8:          </div>
   9:      </div><div class="col-8">
  10:      <button @onclick="neudaten">neudaten</button>
  11:      <h1>Selected @Selected</h1> 
  12:  </div>
  13:  </div>

Durch das Page Routing wird im Code der passende Parameter benötigt. Identer Name und Attribut []. Das Chart Element wird nochmal deklariert (Zeile 5). Durch @ref aus vorigem Listing wird die Verknüpfung aus UI Deklaration und Code hergestellt. So kann dann chart1.daten mit dem String Array verknüpft werden. Allerdings erst spät im Rendervorgang. Im Event OnInitialized wärs zu früh. Deswegen gehts nach Afterrender. Gleichzeitig wird auch das Event aus dem Chart Control abonniert.

Der Benutzer clickt auf ein Segment im Chart und das Event ItemSelected wird ausgelöst. Der Wert zugewiesen und das UI neu gezeichnet.

   1:  @code {
   2:  public string Selected { get; set; }
   3:  [Parameter]
   4:  public string Id { get; set; }
   5:  private Chart chart1;
   6:  public void ItemSelected(string Id)
   7:  {
   8:    Selected = Id;
   9:    StateHasChanged();
  10:  }
  11:  protected override void OnAfterRender(bool firstRender)
  12:  {
  13:    if (firstRender)
  14:          {
  15:              chart1.OnItemSelected += ItemSelected;
  16:              chart1.Daten = new double[] { 10, 200, 34, 50, 100 };
  17:          }
  18:   }
  19:  void neudaten()
  20:   {
  21:     chart1.Daten = new double[] { 100, 20, 340, 50 };
  22:   }

Wieder das Ziel erreicht eine Blazor Anwendung ganz ohne JavaScript. Allerdings erfordert dies ein erhebliches umdenken, wenn man Web Programmierung gewohnt ist. Auch der direkte Vergleich zu UWP und WPF hilft nicht wirklich, auch wenn die Architektur Ansätze durchaus ähnlich sind.

Kommentare sind geschlossen