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.
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)
DemoWie 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:
- Projektinitialisierung
- Erstellen von d3-Schnittstellen für Winkel
- Simulationsgenerierung
- Binden von Simulationsdaten an ein Dokument über einen Winkel
- Benutzerinteraktion an ein Diagramm binden
- Leistungsoptimierung durch Änderungserkennung
- 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 { constructor() {} applyZoomableBehaviour() {} applyDraggableBehaviour() {} 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'); } 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(); } this.simulation.force("centers", d3.forceCenter(options.width / 2, options.height / 2)); 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 KomponentenvorlageEnde 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() { 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"> <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:
- Graphgenerierung und Simulation durch D3
- Binden Sie Simulationsdaten mit Angular an ein Dokument
- 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