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.
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)
DemoComo fazer facilmente nishtyaki tão legal
Abaixo, apresentarei uma abordagem para usar o Angular + D3. Seguiremos as seguintes etapas:
- Inicialização do Projeto
- Criando interfaces d3 para angular
- Geração de simulação
- Vinculando dados de simulação a um documento via angular
- Vinculando a interação do usuário a um gráfico
- Otimização de desempenho através da detecção de alterações
- 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 { constructor() {} applyZoomableBehaviour() {} applyDraggableBehaviour() {} 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'); } 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 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 componenteFim 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() { 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"> <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:
- Geração e simulação de gráficos através do D3
- Vincular dados de simulação a um documento usando Angular
- 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