Archiv für den Monat November 2014

Highcharts mit .NET erstellen und mit AngularJS “wrapper” darstellen


Wer seine Daten mit Charts visualisieren muss hat im Web eine große Auswahl an Anbietern die Chartbibliotheken zur Verfügung stellen. Ich habe dafür bisher immer Google Charts oder Highcharts verwendet. Leider habe ich bisher noch keine alternative Chartbibliothek für AngularJS gefunden, die genauso umfangreich ist wie z.B. Highcharts.

Das schöne an Highcharts, ist das sich die benötigte Datenstruktur sehr einfach mit Hilfe von .NET Klassen abbilden lässt. Wenn man sich die Dokumentation einmal näher anschaut erkennt man schnell das man viele Charts sehr gut mit ein paar .NET Klassen darstellen kann. Diese werden dann beim AJAX Call in JSON konvertiert und können einfach dem Highchart als Datenquelle übergeben werden.

Dies kann z.B. folgendermaßen aussehen:

image

Es muss nur darauf geachtet werden, das die Property Namen kleingeschrieben werden, bzw. genauso geschrieben werden wie in der Dokumentation von Highcharts angegeben.

public class BasicHighChart
{
   public BasicHighChart()
    {
        title = new Title();
        subtitle = new Title();
        chart = new Chart();
        legend = new Legend();
    }

    public Title title { get; set; }

    public Title subtitle { get; set; }

    public Chart chart { get; set; }

    public Legend legend { get; set; }

    public dynamic xAxis { get; set; }

    public dynamic yAxis { get; set; }

    public dynamic series { get; set; }
}

public class Chart
{
    public Chart()
    {
        type = "bar";
        zoomType = string.Empty;
    }

    public string type { get; set; }

    public string zoomType { get; set; }
}

public class Legend
{
    public Legend()
    {
        enabled = true;
        borderWidth = 0;
        layout = "vertical";
        align = "right";
        verticalAlign = "middle";
    }

    public bool enabled { get; set; }

    public string layout { get; set; }

    public string align { get; set; }

    public string verticalAlign { get; set; }

    public int borderWidth { get; set; }
}

...

Das ganze lässt sich grenzenlos ausbauen – je nach dem welche Bedürfnisse man hier abdecken muss.

Die Daten können dann z.B. über den Controller und ein JsonResult zur Verfügung gestellt werden.

 public JsonResult LoadTempratureChartData()
 {
     BasicHighChart chart = new BasicHighChart();
     chart.chart.type = "line";
     chart.chart.zoomType = "x";
     chart.title.text = ".NET Generierte Daten";
     chart.subtitle.text = "chart subtitle";
     chart.legend.enabled = true;
     chart.xAxis = new { categories = new string[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" } };
     chart.yAxis = new { title = new Title() { text = "Temperature (°C)" } };

     var series = new List();
     series.Add(new BasicSeries() { name = "Dresden", data = new List() { 7.0, 4.3, 8.5, 7.7, 19.9, 22.4, 26.1, 31.0, 16.6, 15.2, 6.2, 4.3 } });
     series.Add(new BasicSeries() { name = "Hamburg", data = new List() { 2.2, 7.2, 5.2, 8.3, 18.2, 24.4, 28.9, 32.6, 19.5, 13.8, 3.1, 5.3 } });
     series.Add(new BasicSeries() { name = "Berlin", data = new List() { 5.0, 6.0, 2.8, 9.2, 16.9, 23.5, 29.1, 36.1, 15.2, 12.2, 5.7, 2.6 } });
     chart.series = series;

     return Json(chart, JsonRequestBehavior.AllowGet);
 }

 public JsonResult LoadIllnessChartData()
 {
     BasicHighChart chart = new BasicHighChart();
     chart.chart.type = "line";
     chart.chart.zoomType = "x";
     chart.title.text = ".NET Generierte Daten";
     chart.subtitle.text = "chart subtitle";
     chart.legend.enabled = true;
     chart.xAxis = new { title= new Title() { text = "Krankheitstage"} };
     chart.yAxis = new { title = new Title() { text = "Alter" } };

     var series = new List();
     series.Add(new PointSeries() { name = "Bert", data = new List() { new object[] { 23, 2 }, new object[] { 24, 5 }, new object[] { 25, 8 }, new object[] { 26, 5 }, new object[] { 27, 1 } } });
     series.Add(new PointSeries() { name = "Hugo", data = new List() { new object[] { 23, 8 }, new object[] { 24, 5 }, new object[] { 25, 3 }, new object[] { 26, 4 }, new object[] { 27, 2 } } });
     series.Add(new PointSeries() { name = "Gert", data = new List() { new object[] { 23, 1 }, new object[] { 24, 1 }, new object[] { 25, 2 }, new object[] { 26, 5 }, new object[] { 27, 7 }, new object[] { 28, 2 } } });
     chart.series = series;

     return Json(chart, JsonRequestBehavior.AllowGet);
 }

Damit die Daten auch in AngularJS verwendet werden können empfehle ich für den Anfang eine einfache Direktive, an die z.B. einfach nur die JSON Daten übergeben werden.

app.directive("sqChart", function () {
    return {
        restrict: 'A',
        replace: true,
        scope: {
            chartData: "="
        },
        link: function (scope, element, attr) {
            scope.$watch('chartData.series', function () {
                if (scope.chartData !== undefined && scope.chartData.series !== undefined) {
                    element.highcharts(scope.chartData);
                }
            },true);
        }
    }
});

Die Direktive nimmt über das Attribut “chart-data” die Daten entgegen die dargestellt werden sollen und erstellt ein neues Highcharts Element mit diesen übergebenen Daten. Da wir die Daten nicht gleich beim Aufbau der Seite zur Verfügung haben, sondern diese über AJAX nachladen, wird der watcher in der Direktive benötigt. Damit ist es z.B. auch möglich die Anzeigedaten zu aktualisieren und das Diagramm zeichnet sich automatisch neu.

 <div sq-chart chart-data="ctrl.ViewModel.Temperature"></div>

Die restliche Anwendung besteht dann nur noch daraus die App und den Controller zu definieren und die Daten abzurufen:

var app = angular.module("main.app", []);
app.controller("mainCtrl", function ($http) {
    var that = this;
    this.ViewModel = {};

    $http.get('/Home/LoadTempratureChartData').then(function (result) {
        that.ViewModel.Temperature = result.data;
    });

    $http.get('/Home/LoadIllnessChartData').then(function (result) {
        that.ViewModel.Illness = result.data;
    });
  
});

Mehr ist für das simple Darstellen eines Highcharts Diagramms mit AngularJS nicht notwendig. Hier gibt es bestimmt noch andere alternative Lösungen über Vorschläge freue ich mich immer gern.

Das Beispiel kann wie immer auch bei Codeplex heruntergeladen bzw. der komplette Code eingesehen werden.

Advertisements

AngularJS 1.3 – Neuerungen: one time bindings, ng-messages, ng-model-options


Auch der neueste Release von AngularJS (1.3) hat wieder ein paar interessante Neuerungen mit sich gebracht. Die drei interessantesten Neuerungen meiner Meinung nach sind:

  • Einmalbindungen (“one time bindings”)
  • Die Direktive “ng-messages”
  • Die Erweiterung für “ng-model” mit den “ng-model-options”

1. Einmalbindungen (“one time bindings”)

Wenn man ein Binding in AngularJS über ng-bind oder die Doppeltgeschweiften Klammern erstellt, dann werden diese Werte standardmäßig von AngularJS überwacht und sobald sich dieser Wert ändert, sieht man die Änderungen auch direkt im View.

Oft zeigt man aber Daten an, wo man bereits vorher weiß, das die Werte sich nicht wieder ändern oder bei einem ng-repeat z.B. wo sich maximal die Auflistung ändert aber nicht die einzelnen Werte in der jeweiligen Row selbst. In diesen Fällen werden trotzdem alle Werte überwacht, obwohl dies nicht nötig wäre. Denn aber einer Bestimmten Anzahl an überwachten Bindings kommt es auch zu Performanceproblemen und die Anwendung läuft evtl. nicht mehr “flüssig”.

Daher gibt es ab AngularJS 1.3 die Möglichkeit, dass Bindings nur das erste mal gebunden werden und danach nicht mehr überwacht werden. Dadurch ist es möglich mehr Informationen über AngularJS zu binden und anzuzeigen, ohne das es zu Performanceproblemen kommt. Dies geschieht einfach mit zwei Doppelpunkten vor dem jeweiligen Binding z.B:

<span ng-bind="::viewModel.Name" ></span> 

{{::viewModel.Name}} 

So lange die Variable nicht gesetzt wird (“undefined” ist), wird die Variable überwacht (watch). Wird der Variablen ein Wert zugewiesen, dann wird die Variable nicht mehr überwacht (vereinfacht ausgedrückt) – Details.

Die zwei Doppelpunkte können auch im ng-repeat verwendet werden um z.B. die ganze Schleife nur einmal auszuführen oder auch nur um einzelne Werte einer Row aus der Watch zu entfernen.

<ul class="list-unstyled">;
    <li>
        <div class="row">
            <div class="col-md-6">Name: {{::person.Name}} (Not Watched) | Vorname: {{person.Vorname}} (Watched)/div>;
        </div>
    </li>
</ul>

ACHTUNG HINWEIS: Die one time bindings werden durch das Addon “Batarang” im Chrome “deaktiviert” bzw. funktionieren nicht, d.h. man kann den “::” setzten wo man möchte die Bindings werden aber weiterhin geändert und nicht aus dem Watcher entfernt. Daher entweder das Addon deinstallieren oder einen anderen Browser verwenden.

2. Eine neue Direktive “ng-messages”

Wenn man bisher ein Formular validiert hat und die passenden Meldungen für “required”, “email, “minlength”, “maxength”,, … vernünftig ausgeben wollte, ohne das alle Meldungen gleichzeitig angezeigt werden, war es notwendig dass man teilweise recht aufwendige “ng-if” Anweisungen schreiben musste. Dies wird durch die neue Direktive ng-messages entsprechend vereinfacht.

 <ng-messages for="frm.mail.$error" ng-if="ctrl.submitted">
        <div ng-message="required">
          Bitte eine Eingabe festlegen
        </div>
        <div ng-message="email">
          Bitte eine gültige Email eingeben
        </div>
</ng-messages>

Mit ng-messages und dann der entsprechenden ng-message lassen sich alle Meldungen z.B: für $error für ein Eingabefeld entsprechend einfach darstellen. Die hier angegebene Reihenfolge entspricht ebenfalls der Priorität in der die Meldungen entsprechend angezeigt werden. Dabei wird immer nur eine Meldung angezeigt, was sich aber mit dem Attribut “ng-messages-multiple” wieder deaktivieren lässt – Details.

3. Die Erweiterung für “ng-model” mit den “ng-model-options”

Die neue Direktive lautet “ng-model-options” (Details) und beinhaltet diverse neue Optionen die man setzen kann. Die beiden besten Optionen meiner Meinung nach sind hier “debounce” und “updateOn”. Die neue Direktive kann nur eingesetzt werden in Verbindung mit “ng-model” auf dem gleichen Element und ändert das Bindungsverhalten der gebundenen Modelvariablen entsprechend der angegebenen Optionen.

<input type="email"
      name="mail"
      ng-model="ctrl.Email"
      ng-model-options="{debounce: 500, updateOn: 'blur'}"
      required class="form-control"
      placeholder="Enter email">

Mit der Option “debounce” kann man festlegen wie groß der Zeitraum in Millisekunden sein soll bis der eingegebene Wert tatsächlich ans Model gebunden werden soll. Dies ist z.B: hilfreich wenn man eine Lange Liste mit Werten direkt mit Hilfe von AngularJS filtern möchte und wenn der Filtervorgang immer direkt ausgelöst wird, kann dies durchaus zu Performanceproblemen führen. Mit der Option “debounce” kann man hier entgegen wirken, in dem der Filtervorgang verzögert ausgeführt wird.

Die Option “updateOn” gibt man an ab welchem Ereignis die Werte an das Model gebunden werden sollen, in meinem Beispiel erst wenn der Cursor das Eingabefeld verlässt. Dies eignet sich z.B. sehr gut um Email Adressen erst zu prüfen wenn der Nutzer mit der Eingabe auch fertig ist. Die “updateOn” Option ist daher sehr hilfreich für die Formvalidierung und macht hier vieles einfacher.