Teil 1: Wie man Angular und D3.js zusammen bringt

D3.js für engagierte Angular-Entwickler: Bubble-Charts erstellen
Keine Kommentare

Schwierige Aufgaben stellen sich oft als einfach heraus, wenn man sie anders angeht. Und es hilft, zu wissen, dass es eine einfache Lösung gibt! So auch bei der Arbeit mit D3.js-Charts in Angular, woran so mancher schon verzweifelt ist. Warum eigentlich?

Vor rund einem Jahr habe ich einen Pull-Request für ngx-charts beigetragen. Besonders schwierig war das nicht, sodass für mich klar war, dass es nicht schwierig ist, D3.js mit Angular zu verwenden. Aber jedes Mal, wenn ich meine Kollegen bitte, für mich ein D3.js-Chart in eine Angular-Anwendung einzubauen, treibe ich sie zur Verzweiflung.

Wo liegt das Geheimnis von ngx-charts? Was ist der Trick, um D3.js mit Angular zu kombinieren?

Warum D3.js scheinbar nicht zu Angular passt

Sowohl Angular als auch D3.js sind hervorragend dokumentiert. Daran kann es also nicht liegen. Die D3-Dokumentation richtet sich in erster Linie an erfahrene D3- und JavaScript-Entwickler, aber normalerweise kommen auch Einsteiger nach ein paar Tagen gut zurecht. Für interaktive Charts und Diagramme ist D3.js das Mittel der Wahl.

Für Angular-Entwickler stellt sich die Situation etwas anders dar. Das API von D3.js passt einfach nicht zu Angular. In D3.js dreht sich alles um Daten und Funktionen. In Angular dreht sich alles um die HTML-Templates. Der gemeinsame Nenner fehlt. Auf den ersten Blick ist es unmöglich, die Eigenschaften eines D3-Charts mit den Werten der Attribute einer Angular-Komponente oder eines Service zu verknüpfen.

Dabei ist das Data Binding eines jener Features, die die Entwicklung mit Angular so attraktiv machen. Wir müssen es also so hinbekommen, dass sowohl D3 als auch Angular ihre Stärken ausspielen können. Auf unserem Wunschzettel stehen also sowohl D3-Features wie Animationen und Transitionen als auch Angular-Feature wie z. B. das Data Binding.

Das war jetzt sehr abstrakt. Wir sollten uns das Problem besser anhand des Sourcecodes anschauen. Eines der ersten Diagramme, das wir in unserer Angular-Anwendung gebraucht haben, war ein Bubble-Chart. Der Quelltext ist typisch für eine D3-Anwendung und eignet sich damit als guter Einstieg.

Falls Sie schon Erfahrung mit D3 haben, werden sie im Quelltext ein paar Dinge vermissen. Ich habe den Quelltexte stark vereinfacht. Die fehlenden Programmteile zeige ich weiter unten.

const node = svg
            .selectAll('.node')
            .data(data) // <<<
            .enter()
            .append('g')
            .attr('class', 'node')
            .attr('transform', function(d) {
              return 'translate(' + d.x + ',' + d.y + ')'; // <<<
            });

node
    .append('circle')
    .attr('r', function(d) {
      return d.r; // <<<
    })
    .style('fill', 'red');

Gehen wir den Quelltext Schritt für Schritt durch. Die wichtigsten Zeilen habe ich mit Pfeilen markiert. Der Algorithmus arbeitet mit einem Array von Daten (pack(root).leaves()). Für jedes Array-Element erzeugt er ein Graphikobjekt (append(‚g‘)) und positioniert es auf dem Bildschirm (transform=“‚translate(‚ + d.x + ‚,‘ + d.y + ‚)'“). Bis jetzt kann das Graphikelement alles Mögliche sein. Das ändert sich im letzten Schritt: mit append(‚circle‘) und attr(‚r‘, d.r) wird eine unserer „Bubbles“ erzeugt.

So weit, so gut. Der Algorithmus funktioniert, hat eine gute Struktur, und ist mit etwas Vorwissen leicht zu verstehen.

Als Angular-Entwickler hätte ich aber etwas völlig anderes erwartet:

<circle *ngFor="let d of data" [radius]="d.r" [x]="d.x" [y]="d.y" [color]="red"> </circle>

D3.js verwendet Funktionen, um die Charts aufzubauen. Angular verwendet dafür seine HTML-artige Template-Sprache.

Properties und Events mit einer Angular-Komponente verknüpfen

Natürlich sind das einfach nur zwei unterschiedliche Arten und Weisen, um dasselbe auszudrücken. Ob wir die Funktion append(‚circle’) oder ein HTML-Element circle verwenden, ist ziemlich egal. Aber in einer Angular-Anwendung hat die D3-Syntax zwei große Nachteile. Wir können unser Chart zeichnen, aber es bleibt statisch. Das Killerfeature von Angular geht uns verloren: wir wollen, dass das Chart aktualisiert wird, wenn sich die Daten ändern. Und wir hatten ja vorhin schon überlegt, dass das Data Binding weit oben auf unserer Wunschliste steht.

Der zweite Punkt ist noch wichtiger. Wir können aus D3 heraus keine Angular-Methode aufrufen. Wenn ein Element angeklickt wird, verwendet D3 die JavaScript-Standard-Methode onclick. Die Angular-Variante (click) kennt D3 nicht. Damit sind Probleme vorprogrammiert. D3 umgeht den Angular-Lifecycle. Das ist das gleiche Problem, das es so schwierig macht, eine jQuery-Komponente in einer Angular-Anwendung zu verwenden.

Um es genau zu sagen: Es ist durchaus möglich, aus D3.js heraus eine Angular-Methode aufzurufen. Aber es ist dann nicht klar, was passiert. Das Risiko, dass der Change-Detection-Mechanismus von Angular eine wichtige Änderung nicht mitbekommt, ist groß.

Kurz zusammengefasst, hätten wir gerne so etwas:

<circle *ngFor="let d of data" [radius]="d.r" 
                               [x]="d.x" 
                               [y]="d.y"
                               (click)="d.r = d.r * 2"
> </circle>

Warum nicht einfach auf dem Angular-Weg?

Der Clou an der Sache ist, dass das funktioniert. Es weiß nur keiner. Uns steht die Dokumentation von D3.js im Weg. Jede Dokumentation, die ich bisher gesehen habe, beschreibt nur den algorithmischen Ansatz. Meistens beschreibt sie auch nur, wie etwas gemacht wird, aber nicht, was dabei passiert. Dabei ist es überraschend einfach: D3-Charts sind SVG-Graphiken, die wiederum keine Binärdateien sind, sondern eine Art HTML-Datei. Perfekt geeignet, um in der Template-Language von Angular verwendet zu werden.

In diesem Fall verwenden wir D3.js hauptsächlich, um die Daten zu berechnen – also die Positionen und Größen der Diagrammelemente.

In Angular sieht unser Algorithmus so aus:

<svg class="bubbles">
    <g *ngFor="let d of data" [attr.transform]="'translate('+d.x+','+d.y+')'" 
        (click)="onBubbleSelected(d)">
        <circle [attr.r]="d.r" [attr.fill]="'red'"></circle>
    </g>
</svg>

Jetzt fühlt es sich endlich wie eine richtige Angular-Komponente an! Wir haben einen Chart, dessen Werte aus der Angular-Komponente stammt und das Methoden der Angular-Komponente aufrufen kann. Als Nebeneffekt beginnen wir auch zu verstehen, wie D3.js intern funktioniert. Die Methode enter() in D3.js ist eigentlich nur eine Schleife, die SVG-Elemente erzeugt!

Koordinatenberechnung

Zum Schluss zeige ich doch noch den Teil des Algorithmus, den ich oben weggelassen habe. Es bietet sich an, die Koordinatenberechnung in die Methode ngOnInit() oder ngOnChanges() zu legen. Ich verzichte auf eine detaillierte Erklärung, weil es sich um den Code handelt, den man am Beginn praktisch jedes D3-Charts findet. Wir überlassen D3.js die Berechnung der Datenstruktur und speichern sie in einem Attribut der Komponente (this.data = pack(root).leaves()):

const pack = d3.pack()
               .size([this.width, this.height])
               .padding(1.5);
const root = d3.hierarchy({ children: this.getBubbles() })
               .sum(function(d) {
                   return d.value;
               });
this.data = pack(root).leaves();

Grenzen unseres Ansatzes

Jetzt kümmert sich Angular darum, dass das Diagramm neu gezeichnet wird, wenn sich die Eingabedaten ändern. Es sieht aber noch nicht schön aus. Von einem D3-Chart erwarte ich Animationen und fließende Übergänge zwischen zwei Zuständen. Wenn wir eine Bubble hinzufügen, wird stattdessen das Diagramm komplett neu gezeichnet, ohne jeden Übergang.

Wir haben eine geraume Zeit gebraucht, um zu lernen, wie man das macht. Im dritten Teil dieser Serie wird mein Kollege Christoph Kaden eine elegante Lösung vorstellen.

Fazit

Die Methode, um eines jener coolen D3-Diagramme aus dem Internet in eine Angular-Anwendung zu integrieren, ist selber fast schon ein Algorithmus:

    • Schauen Sie sich das D3-Chart im Browser an.
    • Kopieren Sie die SVG-Knoten, die Angular generiert, in ein Angular-HTML-Template.
    • Verknüpfen Sie die Attributwerte mit den Variablen der Angular-Komponente.
    • Ersetzen Sie die Methode enter() durch eine *ngFor-Schleife.
    • Auf der linken Seite der enter()-Methode steht die Berechnung der Daten, die zum Zeichnen des Charts benötigt werden. Kopieren Sie diesen Code in die ngOnInit()-Methode und speichern Sie das Ergebnis in einem Attribute der Komponente.

Fairerweise sollte ich auch erwähnen, dass es nicht immer so einfach ist. Im nächsten Teil der Serie beschreibe ich, wie ich das Tortendiagramm implementiert habe. Dabei kam eine Komponente heraus, die viel D3-Code und wenig Angular-Code enthält.


Dieser Artikel erschien zuerst auf Englisch auf Beyond Java.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -