Visualisation des données avec Angular et D3

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.

image
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émo

Comment faire facilement un nishtyaki aussi cool


Ci-dessous, je présenterai une approche pour utiliser Angular + D3. Nous allons passer par les étapes suivantes:

  1. Initialisation du projet
  2. Création d'interfaces d3 pour angular
  3. Génération de simulation
  4. Liaison des données de simulation à un document via angulaire
  5. Liaison de l'interaction utilisateur Ă  un graphique
  6. Optimisation des performances grâce à la détection des changements
  7. 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 { /** 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() {} } 

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'); } /** 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(); } } 

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 composant

Fin 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() { /** 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 }; } } 

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"> <!-- 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); } } 

Donc, ce que nous avons finalement:

  1. Génération et simulation de graphes via D3
  2. Lier des données de simulation à un document à l'aide d'Angular
  3. 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

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


All Articles