D3.js est une bibliothèque JavaScript pour manipuler des documents en fonction des données d'entrée. Angular est un cadre qui offre une liaison de données hautes performances.
Ci-dessous, j'examinerai une bonne approche pour exploiter toute cette puissance. Des simulations D3 aux injections SVG et à l'utilisation de la syntaxe des modèles.
Démo: des nombres positifs jusqu'à 300 connectés avec leurs diviseurs.Pour les kulhackers qui ne liront pas cet article, un lien vers le référentiel avec un exemple de code est ci-dessous. Pour tous les autres paysans moyens (bien sûr, ce n'est pas vous) le code de cet article est simplifié pour plus de lisibilité.
Code source (récemment mis à jour vers Angular 5)
DémoComment faire facilement un nishtyaki aussi cool
Ci-dessous, je présenterai une approche pour utiliser Angular + D3. Nous allons passer par les étapes suivantes:
- Initialisation du projet
- Création d'interfaces d3 pour angular
- Génération de simulation
- Liaison des données de simulation à un document via angulaire
- Liaison de l'interaction utilisateur Ă un graphique
- Optimisation des performances grâce à la détection des changements
- Publier et pleurnicher sur la stratégie de versioning angulaire
Alors, ouvrez votre terminal, lancez les éditeurs de code et n'oubliez pas de faire exploser le presse-papiers, nous commençons l'immersion dans le code.
Structure d'application
Nous séparerons le code associé à d3 et svg. Je décrirai plus en détail quand les fichiers nécessaires seront créés, mais pour l'instant, voici la structure de notre future application:
d3 |- models |- directives |- d3.service.ts visuals |- graph |- shared
Initialisation d'une application angulaire
Lancez le projet d'application Angular. Angular 5, 4 ou 2, notre code a été testé sur les trois versions.
Si vous n'avez pas encore angular-cli, installez-le rapidement
npm install -g @angular/cli
Générez ensuite un nouveau projet:
ng new angular-d3-example
Votre application sera créée dans le dossier d'
angular-d3-example
. Exécutez la commande
ng serve
partir de la racine de ce répertoire, l'application sera disponible sur
localhost:4200
.
Initialisation D3
N'oubliez pas d'installer et son annonce TypeSctipt.
npm install --save d3 npm install --save-dev @types/d3
Création d'interfaces d3 pour angular
Pour une utilisation correcte de d3 (ou de toute autre bibliothèque) à l'intérieur du framework, il est préférable d'interagir via une interface personnalisée, que nous définissons via des classes, des services angulaires et des directives. Ce faisant, nous séparerons la fonctionnalité principale des composants qui l'utiliseront. Cela rendra la structure de notre application plus flexible et évolutive, et isolera les bogues.
Notre dossier avec D3 aura la structure suivante:
d3 |- models |- directives |- d3.service.ts
models
assureront la sécurité des types et fourniront des objets de référence.
directives
indiqueront aux éléments comment utiliser la fonctionnalité d3.
d3.service.ts
fournira toutes les méthodes d'utilisation des modèles d3, des directives et également des composants externes de l'application.
Ce service contiendra des modèles de calcul et des comportements. La méthode
getForceDirectedGraph
renverra une instance d'un graphique dirigé. Les
applyDraggableBehaviour
applyZoomableBehaviour
et
applyDraggableBehaviour
vous permettent d'associer les interactions des utilisateurs Ă leurs comportements respectifs.
// path : d3/d3.service.ts import { Injectable } from '@angular/core'; import * as d3 from 'd3'; @Injectable() export class D3Service { constructor() {} applyZoomableBehaviour() {} applyDraggableBehaviour() {} getForceDirectedGraph() {} }
Graphique orienté
Nous procédons à la création d'une classe de graphe orienté et de modèles associés. Notre graphe est constitué de nœuds et de liens, définissons les modèles correspondants.
// 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; } }
Après avoir déclaré les modèles principaux comme manipulation de graphe, déclarons le modèle du graphe lui-même.
// 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(); } }
Puisque nous avons défini nos modèles, mettons également à jour la méthode
D3Service
dans
D3Service
getForceDirectedGraph(nodes: Node[], links: Link[], options: { width, height} ) { let graph = new ForceDirectedGraph(nodes, links, options); return graph; }
La création d'une instance de
ForceDirectedGraph
renverra l'objet suivant
ForceDirectedGraph { ticker: EventEmitter, simulation: Object }
Cet objet contient la propriété de
simulation
avec les données transmises par nous, ainsi que la propriété
ticker
contenant l'émetteur d'événement qui se déclenche à chaque tick de la simulation. Voici comment nous allons l'utiliser:
graph.ticker.subscribe((simulation) => {});
Les méthodes restantes de la classe
D3Service
définies ultérieurement, mais pour l'instant, nous allons essayer de lier les données de l'objet de
simulation
au document.
Liaison de simulation
Nous avons une instance de l'objet ForceDirectedGraph, il contient des données constamment mises à jour des sommets (nœud) et des arcs (lien). Vous pouvez lier ces données à un document de type d3 (comme un sauvage):
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
Composants SVG en angulaire
L'affectation de sélecteurs à des composants qui se trouvent dans l'espace de noms SVG ne fonctionnera pas comme d'habitude. Ils ne peuvent être appliqués que via le sélecteur d'attributs.
<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
Remarquez le préfixe svg dans le modèle de composantFin du diaporama
Liaison de simulation - La partie visuelle
Armé de la connaissance ancienne du svg, nous pouvons commencer à créer des composants qui imiteront nos données. Après les avoir isolés dans le dossier des
visuals
, nous allons créer le dossier
shared
(où nous placerons les composants pouvant être utilisés par d'autres types de graphiques) et le dossier du
graph
principal, qui contiendra tout le code nécessaire pour afficher le graphique orienté (Forcer le graphique dirigé).
visuals |- graph |- shared
Visualisation graphique
Créons notre composant racine, qui va générer un graphique et le lier au document. Nous lui transmettons des nœuds et des liens via les attributs d'entrée du composant.
<graph [nodes]="nodes" [links]="links"></graph>
Le composant accepte les propriétés des
nodes
et des
links
et instancie la classe
ForceDirectedGraph
// 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 }; } }
Composant NodeVisual
Ensuite, ajoutons un composant pour rendre le sommet (nœud), il affichera un cercle avec l'id du sommet.
// 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; }
Composant LinkVisual
Et voici le composant pour visualiser l'arc (lien):
// 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; }
Comportements
Revenons à la partie d3 de l'application, commençons à créer des directives et des méthodes pour le service, ce qui nous donnera des façons intéressantes d'interagir avec le graphique.
Comportement - Zoom
Ajoutez les fixations de la fonction de zoom, afin de pouvoir l'utiliser plus tard facilement:
<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); } }
Comportement - Glisser-déposer
Pour ajouter le glisser-déposer, nous devons avoir accès à l'objet de simulation afin de pouvoir suspendre le dessin pendant le glissement.
<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); } }
Donc, ce que nous avons finalement:
- Génération et simulation de graphes via D3
- Lier des données de simulation à un document à l'aide d'Angular
- Interaction de l'utilisateur avec le graphique via d3
Vous pensez probablement maintenant: "Mes données de simulation changent constamment, angular change constamment ces données dans le document en utilisant la détection de changement, mais pourquoi devrais-je faire cela, je veux mettre à jour le graphique moi-même après chaque tick de la simulation."
Eh bien, vous avez en partie raison, j'ai comparé les résultats des tests de performances avec différents mécanismes de suivi des modifications et il s'avère que lorsque vous appliquez des modifications en privé, nous obtenons un bon gain de performances.
Angulaire, D3 et suivi des modifications (détection des modifications)
Nous allons définir le suivi des modifications dans la méthode onPush (les modifications ne seront suivies que lorsque les liens vers les objets seront complètement remplacés).
Les références aux objets des sommets et des arcs ne changent pas; par conséquent, les modifications ne seront pas suivies. C'est super! Nous pouvons maintenant contrôler le suivi des modifications et le marquer pour des vérifications à chaque tick de la simulation (en utilisant l'émetteur d'événements du ticker que nous avons installé).
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(); }); } }
Maintenant, Angular mettra Ă jour le graphique Ă chaque tick, c'est ce dont nous avons besoin.
C'est tout!
Vous avez survécu à cet article et créé une visualisation cool et évolutive. J'espère que tout était clair et utile. Sinon, faites le moi savoir!
Merci d'avoir lu!
Liran sharir