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.
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)
DemoCara mudah membuat nishtyaki keren seperti itu
Di bawah ini saya akan memperkenalkan satu pendekatan untuk menggunakan Angular + D3. Kami akan melalui langkah-langkah berikut:
- Inisialisasi Proyek
- Membuat antarmuka d3 untuk sudut
- Generasi simulasi
- Mengikat data simulasi ke dokumen melalui sudut
- Mengikat interaksi pengguna ke grafik
- Optimalisasi kinerja melalui deteksi perubahan
- 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 { constructor() {} applyZoomableBehaviour() {} applyDraggableBehaviour() {} 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'); } 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(); } }
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 komponenAkhir 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() { 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"> <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:
- Pembuatan grafik dan simulasi melalui D3
- Bind data simulasi ke dokumen menggunakan Angular
- 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