Visualización de datos con Angular y D3

D3.js es una biblioteca de JavaScript para manipular documentos basados ​​en datos de entrada. Angular es un marco que cuenta con enlace de datos de alto rendimiento.

A continuación, veré un buen enfoque para aprovechar todo este poder. Desde simulaciones D3 hasta inyecciones SVG y uso de sintaxis de plantilla.

imagen
Demostración: números positivos hasta 300 conectados con sus divisores.

Para los kulhackers que no leerán este artículo, a continuación encontrará un enlace al repositorio con un código de ejemplo. Para todos los demás campesinos medios (por supuesto, no eres tú), el código de este artículo está simplificado para facilitar la lectura.

Código fuente (actualizado recientemente a Angular 5)
Demo

Cómo hacer fácilmente nishtyaki tan genial


A continuación, presentaré un enfoque para usar Angular + D3. Vamos a seguir los siguientes pasos:

  1. Inicializacion del proyecto
  2. Crear interfaces d3 para angular
  3. Generación de simulación
  4. Enlace de datos de simulación a un documento a través de angular
  5. Vinculando la interacción del usuario a un gráfico
  6. Optimización del rendimiento mediante detección de cambios.
  7. Publicar y quejarse sobre la estrategia de versiones angulares

Entonces, abra su terminal, inicie los editores de código y no olvide encender el portapapeles, comenzamos la inmersión en el código.

Estructura de aplicación


Vamos a separar el código asociado con d3 y svg. Describiré con más detalle cuándo se crearán los archivos necesarios, pero por ahora, aquí está la estructura de nuestra futura aplicación:

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

Inicializando una Aplicación Angular


Inicie el proyecto de aplicación angular. Angular 5, 4 o 2 nuestro código ha sido probado en las tres versiones.

Si aún no tiene angular-cli, instálelo rápidamente

 npm install -g @angular/cli 

Luego genera un nuevo proyecto:

 ng new angular-d3-example 

Su aplicación se creará en la carpeta angular-d3-example . Ejecute el comando ng serve desde la raíz de este directorio, la aplicación estará disponible en localhost:4200 .

Inicialización D3


Recuerde instalar y su anuncio TypeSctipt.

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

Crear interfaces d3 para angular


Para el uso correcto de d3 (o cualquier otra biblioteca) dentro del marco, es mejor interactuar a través de una interfaz personalizada, que definimos a través de clases, servicios angulares y directivas. Al hacerlo, separaremos la funcionalidad principal de los componentes que la usarán. Esto hará que la estructura de nuestra aplicación sea más flexible y escalable, y aísla errores.

Nuestra carpeta con D3 tendrá la siguiente estructura:

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

models proporcionarán seguridad de tipo y proporcionarán objetos de referencia.
directives le dirán a los elementos cómo usar la funcionalidad d3.
d3.service.ts proporcionará todos los métodos para usar modelos d3, directivas y también componentes externos de la aplicación.

Este servicio contendrá modelos y comportamientos computacionales. El método getForceDirectedGraph devolverá una instancia de un gráfico dirigido. Los applyDraggableBehaviour applyZoomableBehaviour y applyDraggableBehaviour permiten asociar las interacciones del usuario con sus respectivos comportamientos.

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

Gráfico Orientado


Procedemos a crear una clase de gráfico orientado y modelos relacionados. Nuestro gráfico consta de nodos y enlaces, definamos los modelos correspondientes.

 // 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; } } 

Después de declarar los modelos principales como manipulación gráfica, declaremos el modelo del gráfico mismo.

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

Como hemos definido nuestros modelos, también actualice el método D3Service en D3Service

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

Crear una instancia de ForceDirectedGraph devolverá el siguiente objeto

 ForceDirectedGraph { ticker: EventEmitter, simulation: Object } 

Este objeto contiene la propiedad de simulation con los datos pasados ​​por nosotros, así como la propiedad de ticker contiene el emisor de eventos que se dispara con cada tic de la simulación. Así es como lo usaremos:

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

Los métodos restantes de la clase D3Service definirán más adelante, pero por ahora intentaremos vincular los datos del objeto de simulation al documento.

Enlace de simulación


Tenemos una instancia del objeto ForceDirectedGraph, que contiene datos actualizados constantemente de vértices (nodo) y arcos (enlace). Puede vincular estos datos a un documento similar a d3 (como un salvaje):

 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

Componentes SVG en Angular


Asignar selectores a componentes que están en el espacio de nombres SVG no funcionará como de costumbre. Solo se pueden aplicar a través del selector de atributos.

 <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
Observe el prefijo svg en la plantilla del componente

Fin del espectáculo


Enlace de simulación: la parte visual


Armados con el antiguo conocimiento de svg, podemos comenzar a crear componentes que imiten nuestros datos. Después de aislarlos en la carpeta de visuals , crearemos la carpeta shared (donde colocaremos los componentes que pueden ser utilizados por otros tipos de gráficos) y la carpeta de graph principal, que contendrá todo el código necesario para mostrar el gráfico orientado (Gráfico Dirigido a la Fuerza).

 visuals |- graph |- shared 

Visualización gráfica


Creemos nuestro componente raíz, que generará un gráfico y lo vinculará al documento. Pasamos nodos y enlaces a él a través de los atributos de entrada del componente.

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

El componente acepta las propiedades de los nodes y links e instancia la clase 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 }; } } 

Nodo componente Visual


A continuación, agreguemos un componente para representar el vértice (nodo), se mostrará un círculo con la identificación del vértice.

 // 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; } 

Componente LinkVisual


Y aquí está el componente para visualizar el arco (enlace):

 // 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; } 

Comportamientos


Volvamos a la parte d3 de la aplicación, comencemos a crear directivas y métodos para el servicio, lo que nos dará formas geniales de interactuar con el gráfico.

Comportamiento - Zoom


Agregue los enlaces para la función de zoom, para que luego pueda usarse fácilmente:

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

Comportamiento: arrastrar y soltar


Para agregar arrastrar y soltar, necesitamos tener acceso al objeto de simulación para poder pausar el dibujo mientras lo arrastra.

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

Entonces, lo que finalmente tenemos:

  1. Generación de gráficos y simulación a través de D3
  2. Vincula datos de simulación a un documento usando Angular
  3. Interacción del usuario con el gráfico a través de d3

Probablemente piense ahora: "Mis datos de simulación cambian constantemente, angular cambia constantemente estos datos al documento mediante la detección de cambios, pero ¿por qué debería hacer esto? Quiero actualizar el gráfico yo mismo después de cada tic de la simulación".

Bueno, en parte tiene razón, comparé los resultados de las pruebas de rendimiento con diferentes mecanismos para hacer un seguimiento de los cambios y resulta que, al aplicar los cambios de forma privada, obtenemos una buena ganancia de rendimiento.

Angular, D3 y seguimiento de cambios (detección de cambios)


Configuraremos el seguimiento de cambios en el método onPush (los cambios se rastrearán solo cuando los enlaces a los objetos se reemplacen por completo)

Las referencias a objetos de vértices y arcos no cambian; en consecuencia, los cambios no serán rastreados. ¡Esto es genial! Ahora podemos controlar el seguimiento de cambios y marcarlo para verificaciones en cada tick de la simulación (usando el emisor de eventos del ticker que instalamos).

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

Ahora Angular actualizará el gráfico en cada tic, esto es lo que necesitamos.

Eso es todo!


Sobreviviste a este artículo y creaste una visualización genial y escalable. Espero que todo haya sido claro y útil. Si no, ¡házmelo saber!

Gracias por leer!

Liran sharir

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


All Articles