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.
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)
DemoCómo hacer fácilmente nishtyaki tan genial
A continuación, presentaré un enfoque para usar Angular + D3. Vamos a seguir los siguientes pasos:
- Inicializacion del proyecto
- Crear interfaces d3 para angular
- Generación de simulación
- Enlace de datos de simulación a un documento a través de angular
- Vinculando la interacción del usuario a un gráfico
- Optimización del rendimiento mediante detección de cambios.
- 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 { constructor() {} applyZoomableBehaviour() {} applyDraggableBehaviour() {} 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'); } 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(); } }
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 componenteFin 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() { 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"> <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:
- Generación de gráficos y simulación a través de D3
- Vincula datos de simulación a un documento usando Angular
- 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