Visualisasi data dengan Angular dan D3

D3.js adalah perpustakaan JavaScript untuk memanipulasi dokumen berdasarkan input data. Angular adalah kerangka kerja yang menawarkan pengikatan data kinerja tinggi.

Di bawah ini saya akan melihat satu pendekatan yang baik untuk memanfaatkan semua kekuatan ini. Dari simulasi D3 hingga injeksi SVG dan penggunaan sintaks template.

gambar
Demo: angka positif hingga 300 terhubung dengan pembagi mereka.

Untuk kulhackers yang tidak akan membaca artikel ini, tautan ke repositori dengan kode contoh ada di bawah ini. Untuk semua petani menengah lainnya (tentu saja, bukan Anda), kode dalam artikel ini disederhanakan agar mudah dibaca.

Kode Sumber (Baru-Baru Ini Diperbarui ke Sudut 5)
Demo

Cara mudah membuat nishtyaki keren seperti itu


Di bawah ini saya akan memperkenalkan satu pendekatan untuk menggunakan Angular + D3. Kami akan melalui langkah-langkah berikut:

  1. Inisialisasi Proyek
  2. Membuat antarmuka d3 untuk sudut
  3. Generasi simulasi
  4. Mengikat data simulasi ke dokumen melalui sudut
  5. Mengikat interaksi pengguna ke grafik
  6. Optimalisasi kinerja melalui deteksi perubahan
  7. Posting dan merengek tentang strategi versi sudut

Jadi, buka terminal Anda, mulai editor kode dan jangan lupa untuk membuka clipboard, kami mulai membenamkannya dalam kode.

Struktur aplikasi


Kami akan memisahkan kode yang terkait dengan d3 dan svg. Saya akan menjelaskan secara lebih rinci kapan file yang diperlukan akan dibuat, tetapi untuk saat ini, berikut adalah struktur aplikasi kita di masa depan:

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

Menginisialisasi Aplikasi Angular


Luncurkan proyek aplikasi Angular. Sudut 5, 4 atau 2 kode kami telah diuji pada ketiga versi.

Jika Anda belum memiliki angular-cli, instal dengan cepat

 npm install -g @angular/cli 

Kemudian hasilkan proyek baru:

 ng new angular-d3-example 

Aplikasi Anda akan dibuat di folder angular-d3-example . Jalankan perintah ng serve dari root direktori ini, aplikasi akan tersedia di localhost:4200 .

Inisialisasi D3


Ingatlah untuk menginstal dan iklan TypeSctipt-nya.

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

Membuat antarmuka d3 untuk sudut


Untuk penggunaan yang benar dari d3 (atau perpustakaan lain) di dalam kerangka kerja, yang terbaik adalah berinteraksi melalui antarmuka khusus, yang kami definisikan melalui kelas, layanan sudut dan arahan. Dengan melakukan itu, kami akan memisahkan fungsionalitas utama dari komponen yang akan menggunakannya. Ini akan membuat struktur aplikasi kita lebih fleksibel dan terukur, dan mengisolasi bug.

Folder kami dengan D3 akan memiliki struktur berikut:

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

models akan memberikan keamanan tipe dan akan memberikan objek datum.
directives akan memberi tahu elemen bagaimana menggunakan fungsionalitas d3.
d3.service.ts akan menyediakan semua metode untuk menggunakan model d3, arahan, dan juga komponen eksternal aplikasi.

Layanan ini akan berisi model dan perilaku komputasi. Metode getForceDirectedGraph akan mengembalikan instance grafik yang diarahkan. Metode applyZoomableBehaviour dan applyDraggableBehaviour memungkinkan Anda untuk mengaitkan interaksi pengguna dengan perilaku masing-masing.

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

Grafik Berorientasi


Kami melanjutkan untuk membuat kelas grafik berorientasi dan model terkait. Grafik kami terdiri dari node dan tautan, mari kita tentukan model yang sesuai.

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

Setelah mendeklarasikan model utama sebagai manipulasi grafik, mari mendeklarasikan model grafik itu sendiri.

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

Karena kita telah mendefinisikan model kita, mari kita perbarui metode D3Service di D3Service

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

Membuat instance dari ForceDirectedGraph akan mengembalikan objek berikutnya

 ForceDirectedGraph { ticker: EventEmitter, simulation: Object } 

Objek ini berisi properti simulation dengan data yang dikirimkan oleh kami, serta properti ticker berisi peristiwa emitor yang menyala dengan setiap centang pada simulasi. Inilah cara kami akan menggunakannya:

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

Metode yang tersisa dari kelas D3Service ditentukan nanti, tetapi untuk saat ini kami akan mencoba untuk mengikat data objek simulation ke dokumen.

Ikatan simulasi


Kami memiliki turunan dari objek ForceDirectedGraph, yang berisi data simpul (node) dan busur (tautan) yang terus diperbarui. Anda dapat mengikat data ini ke dokumen seperti d3 (seperti orang biadab):

 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

Komponen SVG dalam Angular


Menetapkan pemilih ke komponen yang ada dalam ruang nama SVG tidak akan berfungsi seperti biasa. Mereka hanya bisa diterapkan melalui pemilih atribut.

 <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
Perhatikan awalan svg di templat komponen

Akhir tontonan


Binding Simulasi - Bagian Visual


Berbekal pengetahuan kuno tentang svg, kita dapat mulai membuat komponen yang akan meniru data kita. Setelah mengisolasi mereka dalam folder visuals , maka kita akan membuat folder shared (di mana kita akan menempatkan komponen yang dapat digunakan oleh jenis grafik lainnya) dan folder graph utama, yang akan berisi semua kode yang diperlukan untuk menampilkan grafik berorientasi (Force Directed Graph).

 visuals |- graph |- shared 

Visualisasi grafik


Mari kita buat komponen root kita, yang akan menghasilkan grafik dan mengikatnya ke dokumen. Kami melewati node dan tautan ke sana melalui atribut input komponen.

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

Komponen menerima properti nodes dan links dan instantiates kelas 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 }; } } 

Komponen NodeVisual


Selanjutnya, mari kita tambahkan komponen untuk membuat simpul (simpul), itu akan menampilkan lingkaran dengan id dari simpul.

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

Komponen LinkVisual


Dan di sini adalah komponen untuk memvisualisasikan busur (tautan):

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

Perilaku


Mari kita kembali ke bagian d3 dari aplikasi, mulai membuat arahan dan metode untuk layanan, yang akan memberi kita cara keren untuk berinteraksi dengan grafik.

Perilaku - Zoom


Tambahkan binding untuk fungsi zoom, sehingga nantinya dapat dengan mudah digunakan:

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

Perilaku - Seret dan Jatuhkan


Untuk menambahkan seret dan lepas, kita perlu memiliki akses ke objek simulasi sehingga kita dapat menjeda gambar sambil menyeret.

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

Jadi apa yang akhirnya kita miliki:

  1. Pembuatan grafik dan simulasi melalui D3
  2. Bind data simulasi ke dokumen menggunakan Angular
  3. Interaksi pengguna dengan grafik melalui d3

Anda mungkin berpikir sekarang: "Data simulasi saya terus berubah, sudut terus menerus mengubah data ini ke dokumen menggunakan deteksi perubahan, tetapi mengapa saya harus melakukan ini, saya ingin memperbarui grafik sendiri setelah setiap centang simulasi."

Nah, Anda sebagian benar, saya membandingkan hasil tes kinerja dengan mekanisme berbeda untuk melacak perubahan dan ternyata ketika menerapkan perubahan secara pribadi, kami mendapatkan keuntungan kinerja yang baik.

Angular, D3, dan Change Tracking (Ubah Deteksi)


Kami akan mengatur pelacakan perubahan dalam metode onPush (perubahan hanya akan dilacak ketika tautan ke objek diganti sepenuhnya).

Referensi objek dan simpul tidak berubah, karenanya, perubahan tidak akan dilacak. Ini bagus! Sekarang kita dapat mengontrol pelacakan perubahan dan menandainya untuk pemeriksaan di setiap centang simulasi (menggunakan emitor acara ticker yang kami instal).

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

Sekarang Angular akan memperbarui grafik pada setiap centang, inilah yang kami butuhkan.

Itu saja!


Anda selamat dari artikel ini dan menciptakan visualisasi yang keren dan dapat diukur. Saya harap semuanya jelas dan bermanfaat. Jika tidak, beri tahu saya!

Terima kasih sudah membaca!

Liran sharir

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


All Articles