Visualizing Data with Angular and D3
Binding the data in the data-driven documents.
D3.js - a JavaScript library for manipulating documents based on data. Angular - a framework that prides itself on its high performant data binding techniques.
Below I’ll review one proper approach to harness all that power. From D3’s simulations and forces to SVG injections and template syntax utilization.
The complete code repository is provided below for all the superior developers who will skip reading this article. For all other mediocre developers (definitely not you) note that the code on this page is simplified for readability.
Source: https://github.com/lsharir/angular-d3-graph-example (recently updated to angular 5)
Demo: https://lsharir.github.io/angular-d3-graph-example/
How to easily make cool looking shit like this
Below I will present one approach to harness Angular and D3 together. We will walk through the following steps:
- Initializing a project
- Interfacing d3 through angular
- Generate a simulation
- Bind the simulation’s data to the document through angular
- Bind user interactions to the graph
- Optimize performance by controlling change detection
- Go online and complain about angular versioning strategy
So open your terminals, fire up your editors and warm up the clipboard, we are diving into the code.
Application Structure
We will separate d3 related code and svg visual elements code. I will go into detail when the relevant files will be created, but for now, this is the expected structure of our app:
d3
|- models
|- directives
|- d3.service.tsvisuals
|- graph
|- shared
Initializing an Angular application
Start an Angular application project. Angular 5, 4 or 2 the code below was tested on all of those.
If you don’t have one already, use angular-cli to quickly set one up:
npm install -g @angular/cli
Then start a new angular object:
ng new angular-d3-example
Your application got generated at the angular-d3-example
folder. Use the ng serve
command from its root to start development, the application should be served on localhost:4200
.
Initializing D3
Make sure to install d3 and its relevant types declarations:
npm install --save d3
npm install --save-dev @types/d3
Interfacing D3 through Angular
The correct way to use d3 (or any other library) within a framework is to interact with it through a customized interface, one which we will implement as classes, angular services and directives. By working like that, we separate the core functionalities we are introducing from the components that implement them, making our application structure more flexible and scalable, and isolating bugs in their proper environment.
Our D3 folder will end up looking like this:
d3
|- models
|- directives
|- d3.service.ts
models
will provide us typing safety and robust instances of datum. directives
will direct elements in how to implement d3 behaviors. d3.service.ts
will expose all the methods to be used by either d3 models and directives or external application components.
Let’s start designing the d3 service.
This service will provide us with computational models and behaviors. The getForceDirectedGraph
method will return a force directed graph instance. The applyZoomableBehaviour
and applyDraggableBehaviour
methods will let us bind user interactions with elements to corresponding behaviors.
Force Directed Graph
Let’s start creating our force directed graph class and its relevant models. Our graph consists of nodes and links, let’s define the appropriate models.
After defining the core models for our graph to manipulate, we continue to define the graph model itself.
Once these models are defined, we can update our getForceDirectedGraph
method in our D3Service
Creating an instance of ForceDirectedGraph
will return an instance
ForceDirectedGraph {
ticker: EventEmitter,
simulation: Object
}
It contains a running simulation
with the data we provided, along with a ticker
holding an event emitter that fires every time the simulation ticks, which we will use like this:
graph.ticker.subscribe((simulation) => {});
We’ll implement the rest of the methods of the D3Service
later, we’re going to try and bind the simulation
data into the document.
Binding the simulation
We got a hold of our ForceDirectedGraph
instance. It holds an ever changing precious data of nodes and their connecting links. You could bind that data to the document the d3 way (like a savage):
function ticked() {
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
Luckily, this is the 21st century, humanity has evolved to use efficient data binding tools instead of mindlessly changing attributes on elements. This is where Angular’s .muscles { display: flex }
Intermezzo: SVG and Angular
SVG templating with Angular
SVG delayed implementation resulted in a restricting namespace separation within the html document. Which is why Angular cannot recognize declared SVG elements in our components templates (Unless they are visibly descendants of an <svg>
tag).
To compile our svg elements properly, we must either:
- Meticulously keep them visibly nested under the
<svg>
tag. - prefix them with “svg” to let Angular know what’s up
<svg:line>
SVG components with Angular
Assigning selectors to components in the SVG namespace will not work the usual way. They must be selected through an attribute selector
End of intermezzo
Binding the simulation — visuals
Equipped with ancient svg knowledge, we can start creating visual components that will display our data. Isolated in a visuals
folder, we will create shared
components (to be potentially used by other graphs) and our primary graph
folder, that will hold all the code required to display our force directed graph.
visuals
|- graph
|- shared
Graph visuals
Let’s create the root component that will generate the graph and bind it to the document. We pass along the nodes and links data as the components input variables.
<graph [nodes]="nodes" [links]="links"></graph>
The component takes the nodes
and links
and creates an instance of the ForceDirectedGraph
Node visual component
Next, let’s create the node visual component, it will display a circle and the id of the node.
Link visual component
On to the link visual component:
Behaviors
Getting back to the d3 part of the application, let’s start creating the directives and service methods required to add cool behaviors to the graph.
Behavior — pan and zoom
We create hooks for the pan and zoom behavior, so it can be easily used:
<svg #svg>
<g [zoomableOf]="svg">
<!-- nodes and links -->
</g>
</svg>
Behavior — draggable
For a draggable behavior, we want to provide the simulation so it can be paused while dragging is in progress.
<svg #svg>
<g [zoomableOf]="svg">
<!-- links -->
<g [nodeVisual]="node"
*ngFor="let node of nodes"
[draggableNode]="node"
[draggableInGraph]="graph">
</g>
</g>
</svg>
Let’s recap, our app can now:
- Generate a graph and simulation through D3
- Bind the simulation data to the document with Angular
- Bind user interactions with elements to d3 behaviors
You are probably now wondering: “My simulation data is constantly changing, angular’s magical change detection is binding that data to the document, but why should I let it, I want to force refresh the graph only after simulation ticks.”
Well, you are sort of right, I compared a custom tests with different change detection strategies and there is a noticeable improvement when we take control of change detection.
Angular, D3 and Change Detection
Set the change detection to onPush (change is detected only when the reference of the objects was completely replaced).
The nodes and links object reference does not change, and no change will be detected. This is great! we can now take control of change detection and mark it for check on every simulation tick (using the ticker event emitter we set up earlier).
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();
});
}
}
Angular will now refresh the graph elements on every tick, which is exactly what we wanted.
That’s it!
You have successfully survived this article and made a cool and scalable visualization. I hope it was clear, useful, correct and concise. If it wasn’t — let me know!
Thank you for reading!