Visualização de dados com Angular e D3

D3.js é uma biblioteca JavaScript para manipular documentos com base em dados de entrada. Angular é uma estrutura que possui ligação de dados de alto desempenho.

Abaixo, examinarei uma boa abordagem para aproveitar todo esse poder. Das simulações D3 às injeções de SVG e uso da sintaxe do modelo.

imagem
Demo: números positivos de até 300 conectados aos seus divisores.

Para kulhackers que não lerão este artigo, um link para o repositório com um código de exemplo está abaixo. Para todos os outros camponeses do meio (é claro que não é você), o código deste artigo é simplificado para facilitar a leitura.

Código fonte (atualizado recentemente para o Angular 5)
Demo

Como fazer facilmente nishtyaki tão legal


Abaixo, apresentarei uma abordagem para usar o Angular + D3. Seguiremos as seguintes etapas:

  1. Inicialização do Projeto
  2. Criando interfaces d3 para angular
  3. Geração de simulação
  4. Vinculando dados de simulação a um documento via angular
  5. Vinculando a interação do usuário a um gráfico
  6. Otimização de desempenho através da detecção de alterações
  7. Postagem e reclamações sobre a estratégia de versão angular

Então, abra seu terminal, inicie os editores de código e não se esqueça de aumentar a área de transferência, começamos a imersão no código.

Estrutura de aplicação


Separaremos o código associado a d3 e svg. Descreverei com mais detalhes quando os arquivos necessários serão criados, mas, por enquanto, aqui está a estrutura do nosso futuro aplicativo:

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

Inicializando um aplicativo angular


Inicie o projeto de aplicativo Angular. Angular 5, 4 ou 2, nosso código foi testado nas três versões.

Se você ainda não possui o angular-cli, instale-o rapidamente

 npm install -g @angular/cli 

Em seguida, gere um novo projeto:

 ng new angular-d3-example 

Seu aplicativo será criado na pasta angular-d3-example . Execute o comando ng serve partir da raiz deste diretório, o aplicativo estará disponível no localhost:4200 .

Inicialização D3


Lembre-se de instalar e seu anúncio TypeSctipt.

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

Criando interfaces d3 para angular


Para o uso correto do d3 (ou de qualquer outra biblioteca) dentro da estrutura, é melhor interagir através de uma interface personalizada, definida por meio de classes, serviços angulares e diretivas. Ao fazer isso, separaremos a funcionalidade principal dos componentes que a usarão. Isso tornará a estrutura de nosso aplicativo mais flexível e escalável e isola os bugs.

Nossa pasta com D3 terá a seguinte estrutura:

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

models fornecerão segurança de tipo e objetos de referência.
directives dirão aos elementos como usar a funcionalidade d3.
d3.service.ts fornecerá todos os métodos para usar modelos d3, diretivas e também componentes externos do aplicativo.

Este serviço conterá modelos e comportamentos computacionais. O método getForceDirectedGraph retornará uma instância de um gráfico direcionado. Os applyDraggableBehaviour e applyDraggableBehaviour permitem associar interações do usuário com seus respectivos comportamentos.

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


Continuamos a criar uma classe de gráfico orientado e modelos relacionados. Nosso gráfico consiste em nós e links, vamos definir os modelos correspondentes.

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

Depois de declarar os principais modelos como manipulação de gráfico, vamos declarar o modelo do próprio gráfico.

 // 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 definimos nossos modelos, também vamos atualizar o método D3Service no D3Service

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

Criar uma instância do ForceDirectedGraph retornará o próximo objeto

 ForceDirectedGraph { ticker: EventEmitter, simulation: Object } 

Este objeto contém a propriedade de simulation com os dados passados ​​por nós, bem como a propriedade de ticker contém o emissor de eventos que é acionado a cada tick da simulação. Aqui está como vamos usá-lo:

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

Os métodos restantes da classe D3Service definidos posteriormente, mas por enquanto tentaremos vincular os dados do objeto de simulation ao documento.

Ligação de simulação


Temos uma instância do objeto ForceDirectedGraph, que contém dados constantemente atualizados de vértices (nó) e arcos (link). Você pode vincular esses dados a um documento do tipo d3 (como um selvagem):

 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 em Angular


A atribuição de seletores a componentes que estão no espaço de nome SVG não funcionará normalmente. Eles podem ser aplicados apenas através do seletor 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 o prefixo svg no modelo de componente

Fim da exibição lateral


Ligação de simulação - A parte visual


Armado com o conhecimento antigo de svg, podemos começar a criar componentes que imitarão nossos dados. Após isolá-los na pasta visuals , criaremos a pasta shared (onde colocaremos os componentes que podem ser usados ​​por outros tipos de gráficos) e a pasta principal do graph , que conterá todo o código necessário para exibir o gráfico orientado (Force Directed Graph).

 visuals |- graph |- shared 

Visualização de gráficos


Vamos criar nosso componente raiz, que irá gerar um gráfico e vinculá-lo ao documento. Passamos nós e links para ele através dos atributos de entrada do componente.

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

O componente aceita as propriedades de nodes e links e instancia a 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 }; } } 

Componente NodeVisual


Em seguida, vamos adicionar um componente para renderizar o vértice (nó), ele exibirá um círculo com o ID do 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


E aqui está o componente para visualizar o arco (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; } 

Comportamentos


Vamos voltar à parte d3 do aplicativo, começar a criar diretrizes e métodos para o serviço, o que nos dará maneiras legais de interagir com o gráfico.

Comportamento - Zoom


Adicione as ligações para a função de zoom, para que mais tarde possa ser facilmente usada:

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

Comportamento - Arrastar e Soltar


Para adicionar arrastar e soltar, precisamos ter acesso ao objeto de simulação para que possamos pausar o desenho enquanto arrasta.

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

Então, o que finalmente temos:

  1. Geração e simulação de gráficos através do D3
  2. Vincular dados de simulação a um documento usando Angular
  3. Interação do usuário com o gráfico através de d3

Você provavelmente pensa agora: "Meus dados de simulação estão mudando constantemente, o angular muda constantemente esses dados para o documento usando a detecção de alterações, mas por que devo fazer isso? Quero atualizar o gráfico depois de cada marca da simulação".

Bem, você está parcialmente certo, comparei os resultados de testes de desempenho com diferentes mecanismos para rastrear alterações e, ao aplicar as alterações em particular, obtemos um bom ganho de desempenho.

Angular, D3 e rastreamento de alterações (detecção de alterações)


Definiremos o rastreamento de alterações no método onPush (as alterações serão rastreadas somente quando os links para objetos forem completamente substituídos).

As referências a objetos de vértices e arcos não são alteradas; portanto, as alterações não serão rastreadas. Isso é ótimo! Agora podemos controlar o rastreamento de alterações e marcá-lo para verificações em cada tick da simulação (usando o emissor de evento do 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(); }); } } 

Agora o Angular atualizará o gráfico a cada tick, é disso que precisamos.

Isso é tudo!


Você sobreviveu a este artigo e criou uma visualização legal e escalável. Espero que tudo tenha sido claro e útil. Se não, me avise!

Obrigado pela leitura!

Liran sharir

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


All Articles