Datenvisualisierung mit Angular und D3

D3.js ist eine JavaScript-Bibliothek zum Bearbeiten von Dokumenten basierend auf Eingabedaten. Angular ist ein Framework mit leistungsstarker Datenbindung.

Im Folgenden werde ich einen guten Ansatz zur Nutzung all dieser Kräfte betrachten. Von D3-Simulationen über SVG-Injektionen bis hin zur Verwendung der Template-Syntax.

Bild
Demo: Positive Zahlen bis zu 300, die mit ihren Teilern verbunden sind.

Für Kulhacker, die diesen Artikel nicht lesen, finden Sie unten einen Link zum Repository mit einem Beispielcode. Für alle anderen Mittelbauern (natürlich nicht für Sie) ist der Code in diesem Artikel zur besseren Lesbarkeit vereinfacht.

Quellcode (kürzlich auf Angular 5 aktualisiert)
Demo

Wie man so coole Nishtyaki leicht macht


Im Folgenden werde ich einen Ansatz zur Verwendung von Angular + D3 vorstellen. Wir werden die folgenden Schritte ausführen:

  1. Projektinitialisierung
  2. Erstellen von d3-Schnittstellen für Winkel
  3. Simulationsgenerierung
  4. Binden von Simulationsdaten an ein Dokument über einen Winkel
  5. Benutzerinteraktion an ein Diagramm binden
  6. Leistungsoptimierung durch Änderungserkennung
  7. Posting und Jammern über eckige Versionierungsstrategie

Öffnen Sie also Ihr Terminal, starten Sie die Code-Editoren und vergessen Sie nicht, die Zwischenablage zu öffnen. Wir beginnen mit dem Eintauchen in den Code.

Anwendungsstruktur


Wir werden den mit d3 und svg verbundenen Code trennen. Ich werde detaillierter beschreiben, wann die erforderlichen Dateien erstellt werden, aber im Moment ist hier die Struktur unserer zukünftigen Anwendung:

d3 |- models |- directives |- d3.service.ts visuals |- graph |- shared 

Initialisieren einer Winkelanwendung


Starten Sie das Angular-Anwendungsprojekt. Winkel 5, 4 oder 2 Unser Code wurde auf allen drei Versionen getestet.

Wenn Sie noch kein Angular-Cli haben, installieren Sie es schnell

 npm install -g @angular/cli 

Dann generieren Sie ein neues Projekt:

 ng new angular-d3-example 

Ihre Anwendung wird im Ordner angular-d3-example . Führen Sie den Befehl ng serve im Stammverzeichnis dieses Verzeichnisses aus. Die Anwendung ist unter localhost:4200 verfügbar.

Initialisierung D3


Denken Sie daran, die TypeSctipt-Anzeige zu installieren.

 npm install --save d3 npm install --save-dev @types/d3 

Erstellen von d3-Schnittstellen für Winkel


Für die korrekte Verwendung von d3 (oder anderen Bibliotheken) innerhalb des Frameworks ist es am besten, über eine benutzerdefinierte Schnittstelle zu interagieren, die wir durch Klassen, Winkeldienste und Direktiven definieren. Dabei trennen wir die Hauptfunktionalität von den Komponenten, die sie verwenden. Dadurch wird die Struktur unserer Anwendung flexibler und skalierbarer und Fehler werden isoliert.

Unser Ordner mit D3 hat folgende Struktur:

 d3 |- models |- directives |- d3.service.ts 

models bieten Typensicherheit und Bezugsobjekte.
directives geben den Elementen an, wie die d3-Funktionalität verwendet werden soll.
d3.service.ts bietet alle Methoden zur Verwendung von d3-Modellen, Direktiven und auch externen Komponenten der Anwendung.

Dieser Dienst enthält Rechenmodelle und Verhaltensweisen. Die Methode getForceDirectedGraph gibt eine Instanz eines gerichteten Graphen zurück. Mit den applyDraggableBehaviour applyZoomableBehaviour und applyDraggableBehaviour Sie Benutzerinteraktionen mit ihrem jeweiligen Verhalten verknüpfen.

 // path : d3/d3.service.ts import { Injectable } from '@angular/core'; import * as d3 from 'd3'; @Injectable() export class D3Service { /** This service will provide methods to enable user interaction with elements * while maintaining the d3 simulations physics */ constructor() {} /** A method to bind a pan and zoom behaviour to an svg element */ applyZoomableBehaviour() {} /** A method to bind a draggable behaviour to an svg element */ applyDraggableBehaviour() {} /** The interactable graph we will simulate in this article * This method does not interact with the document, purely physical calculations with d3 */ getForceDirectedGraph() {} } 

Orientierter Graph


Wir fahren fort, eine Klasse von orientierten Graphen und verwandten Modellen zu erstellen. Unser Diagramm besteht aus Knoten und Links. Definieren wir die entsprechenden Modelle.

 // path : d3/models/index.ts export * from './node'; export * from './link'; // To be implemented in the next gist export * from './force-directed-graph'; 

 // path : d3/models/link.ts import { Node } from './'; // Implementing SimulationLinkDatum interface into our custom Link class export class Link implements d3.SimulationLinkDatum<Node> { // Optional - defining optional implementation properties - required for relevant typing assistance index?: number; // Must - defining enforced implementation properties source: Node | string | number; target: Node | string | number; constructor(source, target) { this.source = source; this.target = target; } } 

 // path : d3/models/node.ts // Implementing SimulationNodeDatum interface into our custom Node class export class Node extends d3.SimulationNodeDatum { // Optional - defining optional implementation properties - required for relevant typing assistance index?: number; x?: number; y?: number; vx?: number; vy?: number; fx?: number | null; fy?: number | null; id: string; constructor(id) { this.id = id; } } 

Nachdem Sie die Hauptmodelle als Diagrammmanipulation deklariert haben, deklarieren wir das Modell des Diagramms selbst.

 // path : d3/models/force-directed-graph.ts import { EventEmitter } from '@angular/core'; import { Link } from './link'; import { Node } from './node'; import * as d3 from 'd3'; const FORCES = { LINKS: 1 / 50, COLLISION: 1, CHARGE: -1 } export class ForceDirectedGraph { public ticker: EventEmitter<d3.Simulation<Node, Link>> = new EventEmitter(); public simulation: d3.Simulation<any, any>; public nodes: Node[] = []; public links: Link[] = []; constructor(nodes, links, options: { width, height }) { this.nodes = nodes; this.links = links; this.initSimulation(options); } initNodes() { if (!this.simulation) { throw new Error('simulation was not initialized yet'); } this.simulation.nodes(this.nodes); } initLinks() { if (!this.simulation) { throw new Error('simulation was not initialized yet'); } // Initializing the links force simulation this.simulation.force('links', d3.forceLink(this.links) .strength(FORCES.LINKS) ); } initSimulation(options) { if (!options || !options.width || !options.height) { throw new Error('missing options when initializing simulation'); } /** Creating the simulation */ if (!this.simulation) { const ticker = this.ticker; // Creating the force simulation and defining the charges this.simulation = d3.forceSimulation() .force("charge", d3.forceManyBody() .strength(FORCES.CHARGE) ); // Connecting the d3 ticker to an angular event emitter this.simulation.on('tick', function () { ticker.emit(this); }); this.initNodes(); this.initLinks(); } /** Updating the central force of the simulation */ this.simulation.force("centers", d3.forceCenter(options.width / 2, options.height / 2)); /** Restarting the simulation internal timer */ this.simulation.restart(); } } 

Da wir unsere Modelle definiert haben, aktualisieren wir auch die Methode D3Service in D3Service

 getForceDirectedGraph(nodes: Node[], links: Link[], options: { width, height} ) { let graph = new ForceDirectedGraph(nodes, links, options); return graph; } 

Wenn Sie eine Instanz von ForceDirectedGraph wird das nächste Objekt zurückgegeben

 ForceDirectedGraph { ticker: EventEmitter, simulation: Object } 

Dieses Objekt enthält die simulation mit den von uns übergebenen Daten sowie die ticker mit dem Ereignisemitter, der bei jedem Tick der Simulation ausgelöst wird. So werden wir es verwenden:

 graph.ticker.subscribe((simulation) => {}); 

Die verbleibenden Methoden der D3Service Klasse D3Service später definiert. Im D3Service wir jedoch versuchen, die Daten des simulation an das Dokument zu binden.

Simulationsbindung


Wir haben eine Instanz des ForceDirectedGraph-Objekts, das ständig aktualisierte Daten von Eckpunkten (Knoten) und Bögen (Link) enthält. Sie können diese Daten an ein d3-ähnliches Dokument binden (wie ein Wilder):

 function ticked() { node .attr("cx", function(d) { return dx; }) .attr("cy", function(d) { return dy; }); }<source>  ,   21 ,        ,     .   Angular   . <h3><i>: SVG  Angular</i></h3> <h3>SVG   Angular</h3>   SVG,       svg  html .   Angular     SVG    Angular  (        <code>svg</code>).     SVG      : <ol> <li>      <code>svg</code>.</li> <li>  “svg”,   Angular',  <code><svg:line></code></li> </ol> <source lang="xml"> <svg> <line x1="0" y1="0" x2="100" y2="100"></line> </svg> 

app.component.html

 <svg:line x1="0" y1="0" x2="100" y2="100"></svg:line> 

link-example.component.html

SVG-Komponenten in Angular


Das Zuweisen von Selektoren zu Komponenten im SVG-Namespace funktioniert nicht wie gewohnt. Sie können nur über die Attributauswahl angewendet werden.

 <svg> <g [lineExample]></g> </svg> 

app.component.html

 import { Component } from '@angular/core'; @Component({ selector: '[lineExample]', template: `<svg:line x1="0" y1="0" x2="100" y2="100"></svg:line>` }) export class LineExampleComponent { constructor() {} } 

link-example.component.ts
Beachten Sie das svg-Präfix in der Komponentenvorlage

Ende der Nebenschau


Simulationsbindung - Der visuelle Teil


Mit dem alten Wissen von svg können wir beginnen, Komponenten zu erstellen, die unsere Daten nachahmen. Nachdem wir sie im visuals Ordner isoliert haben, erstellen wir den shared Ordner (in dem wir die Komponenten platzieren, die von anderen Diagrammtypen verwendet werden können) und den Hauptdiagrammordner, der den gesamten Code enthält, der zum Anzeigen des orientierten Diagramms erforderlich ist (Force Directed Graph).

 visuals |- graph |- shared 

Grafikvisualisierung


Erstellen wir unsere Stammkomponente, die ein Diagramm generiert und an das Dokument bindet. Wir übergeben Knoten und Links über die Eingabeattribute der Komponente.

 <graph [nodes]="nodes" [links]="links"></graph> 

Die Komponente akzeptiert die nodes und links und instanziiert die ForceDirectedGraph Klasse

 // path : visuals/graph/graph.component.ts import { Component, Input } from '@angular/core'; import { D3Service, ForceDirectedGraph, Node } from '../../d3'; @Component({ selector: 'graph', template: ` <svg #svg [attr.width]="_options.width" [attr.height]="_options.height"> <g> <g [linkVisual]="link" *ngFor="let link of links"></g> <g [nodeVisual]="node" *ngFor="let node of nodes"></g> </g> </svg> `, styleUrls: ['./graph.component.css'] }) export class GraphComponent { @Input('nodes') nodes; @Input('links') links; graph: ForceDirectedGraph; constructor(private d3Service: D3Service) { } ngOnInit() { /** Receiving an initialized simulated graph from our custom d3 service */ this.graph = this.d3Service.getForceDirectedGraph(this.nodes, this.links, this.options); } ngAfterViewInit() { this.graph.initSimulation(this.options); } private _options: { width, height } = { width: 800, height: 600 }; get options() { return this._options = { width: window.innerWidth, height: window.innerHeight }; } } 

Komponente NodeVisual


Als nächstes fügen wir eine Komponente hinzu, um den Scheitelpunkt (Knoten) zu rendern. Daraufhin wird ein Kreis mit der ID des Scheitelpunkts angezeigt.

 // path : visuals/shared/node-visual.component.ts import { Component, Input } from '@angular/core'; import { Node } from '../../../d3'; @Component({ selector: '[nodeVisual]', template: ` <svg:g [attr.transform]="'translate(' + node.x + ',' + node.y + ')'"> <svg:circle cx="0" cy="0" r="50"> </svg:circle> <svg:text> {{node.id}} </svg:text> </svg:g> ` }) export class NodeVisualComponent { @Input('nodeVisual') node: Node; } 

LinkVisual-Komponente


Und hier ist die Komponente zur Visualisierung des Bogens (Link):

 // path : visuals/shared/link-visual.component.ts import { Component, Input } from '@angular/core'; import { Link } from '../../../d3'; @Component({ selector: '[linkVisual]', template: ` <svg:line [attr.x1]="link.source.x" [attr.y1]="link.source.y" [attr.x2]="link.target.x" [attr.y2]="link.target.y" ></svg:line> ` }) export class LinkVisualComponent { @Input('linkVisual') link: Link; } 

Verhalten


Kehren wir zum d3-Teil der Anwendung zurück und beginnen mit der Erstellung von Anweisungen und Methoden für den Dienst, die uns coole Möglichkeiten zur Interaktion mit dem Diagramm bieten.

Verhalten - Zoom


Fügen Sie die Bindungen für die Zoomfunktion hinzu, damit sie später problemlos verwendet werden kann:

 <svg #svg> <g [zoomableOf]="svg"></g> </svg> 

 // path : d3/d3.service.ts // ... export class D3Service { applyZoomableBehaviour(svgElement, containerElement) { let svg, container, zoomed, zoom; svg = d3.select(svgElement); container = d3.select(containerElement); zoomed = () => { const transform = d3.event.transform; container.attr("transform", "translate(" + transform.x + "," + transform.y + ") scale(" + transform.k + ")"); } zoom = d3.zoom().on("zoom", zoomed); svg.call(zoom); } // ... } 

 // path : d3/directives/zoomable.directive.ts import { Directive, Input, ElementRef } from '@angular/core'; import { D3Service } from '../d3.service'; @Directive({ selector: '[zoomableOf]' }) export class ZoomableDirective { @Input('zoomableOf') zoomableOf: ElementRef; constructor(private d3Service: D3Service, private _element: ElementRef) {} ngOnInit() { this.d3Service.applyZoomableBehaviour(this.zoomableOf, this._element.nativeElement); } } 

Verhalten - Drag & Drop


Um Drag & Drop hinzuzufügen, müssen wir Zugriff auf das Simulationsobjekt haben, damit wir das Zeichnen beim Ziehen anhalten können.

 <svg #svg> <g [zoomableOf]="svg"> <!-- links --> <g [nodeVisual]="node" *ngFor="let node of nodes" [draggableNode]="node" [draggableInGraph]="graph"> </g> </g> </svg> 

 // path : d3/d3.service.ts // ... export class D3Service { applyDraggableBehaviour(element, node: Node, graph: ForceDirectedGraph) { const d3element = d3.select(element); function started() { /** Preventing propagation of dragstart to parent elements */ d3.event.sourceEvent.stopPropagation(); if (!d3.event.active) { graph.simulation.alphaTarget(0.3).restart(); } d3.event.on("drag", dragged).on("end", ended); function dragged() { node.fx = d3.event.x; node.fy = d3.event.y; } function ended() { if (!d3.event.active) { graph.simulation.alphaTarget(0); } node.fx = null; node.fy = null; } } d3element.call(d3.drag() .on("start", started)); } // ... } 

 // path : d3/directives/draggable.directives.ts import { Directive, Input, ElementRef } from '@angular/core'; import { Node, ForceDirectedGraph } from '../models'; import { D3Service } from '../d3.service'; @Directive({ selector: '[draggableNode]' }) export class DraggableDirective { @Input('draggableNode') draggableNode: Node; @Input('draggableInGraph') draggableInGraph: ForceDirectedGraph; constructor(private d3Service: D3Service, private _element: ElementRef) { } ngOnInit() { this.d3Service.applyDraggableBehaviour(this._element.nativeElement, this.draggableNode, this.draggableInGraph); } } 

Also, was wir letztendlich haben:

  1. Graphgenerierung und Simulation durch D3
  2. Binden Sie Simulationsdaten mit Angular an ein Dokument
  3. Benutzerinteraktion mit dem Diagramm über d3

Sie denken jetzt wahrscheinlich: "Meine Simulationsdaten ändern sich ständig, Winkel ändert diese Daten mithilfe der Änderungserkennung ständig in das Dokument. Warum sollte ich dies tun, ich möchte das Diagramm nach jedem Tick der Simulation selbst aktualisieren."

Nun, Sie haben teilweise Recht, ich habe die Ergebnisse von Leistungstests mit verschiedenen Mechanismen zur Verfolgung von Änderungen verglichen, und es stellt sich heraus, dass wir bei der privaten Anwendung von Änderungen einen guten Leistungsgewinn erzielen.

Winkel-, D3- und Änderungsverfolgung (Änderungserkennung)


Wir werden die Änderungsverfolgung in der onPush-Methode festlegen (Änderungen werden nur verfolgt, wenn Links zu Objekten vollständig ersetzt wurden).

Verweise auf Objekte mit Scheitelpunkten und Bögen ändern sich nicht. Dementsprechend werden Änderungen nicht verfolgt. Das ist großartig! Jetzt können wir die Änderungsverfolgung steuern und sie bei jedem Tick der Simulation für Überprüfungen markieren (unter Verwendung des Ereignisemitters des von uns installierten Tickers).

 import { Component, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'graph', changeDetection: ChangeDetectionStrategy.OnPush, template: `<!-- svg, nodes and links visuals -->` }) export class GraphComponent { constructor(private ref: ChangeDetectorRef) { } ngOnInit() { this.graph = this.d3Service.getForceDirectedGraph(...); this.graph.ticker.subscribe((d) => { this.ref.markForCheck(); }); } } 

Jetzt aktualisiert Angular das Diagramm bei jedem Tick. Dies ist das, was wir brauchen.

Das ist alles!


Sie haben diesen Artikel überlebt und eine coole, skalierbare Visualisierung erstellt. Ich hoffe, dass alles klar und nützlich war. Wenn nicht, lass es mich wissen!

Danke fürs Lesen!

Liran Sharir

Source: https://habr.com/ru/post/de414785/


All Articles