In this 2 part article, I will discuss D3 along with Angular. D3 is a powerful JavaScript library that enables you to develop interactive data visualizations. Visualizations often include charts, network diagrams, maps, among others. There are many libraries available for Angular that have nice and easy to work with components and directives that you can use to build different kinds of charts. However, let’s suppose that you would like to depict in a map the areas where you would be more likely to find single people, or make an interactive network diagram of beneficiaries of a certain organization. In those cases is where D3 comes in handy.
A common misconception about D3 is that it is conceived as a charting library. While it is true that you can build any kind of chart using D3, its scope goes way beyond that. “D3.js is a JavaScript library for manipulating documents based on data” (D3.js 2021). This means that you can build not only charts but also create any kind of data visualization.
In my experience, there are times when a project requirement needs to be a custom visualization such as a map, a venue, or a network diagram. Often, these requirements need that you build that customization. Let’s look at a basic example of how this can be done in an Angular project.
Set up project
1. Create New Project
The first thing will be to start a new project.
ng new d3-example
2. Install dependencies
Once the new project is created we add the dependencies that we will need for this example. For this particular project, we will use Bootstrap for styling and D3 to build a line chart.
npm install bootstrap d3 –save
Line Chart Example
The visualization example is a simple line chart that depicts the market value of Bitcoin over a period of time.
1. Edit app.component.html
Let’s go ahead and replace the following code in app.component.html
<nav class=”navbar navbar-expand-lg navbar-light bg-light”> <div class=”container”> <a class=”navbar-brand” href=”#”>Angular D3 Example</a> </div> </nav> <div class=”container mt-3″> <!– TODO: chart container goes here –> </div> |
This should leave us with a nice navigation bar and the container where the chart will be.
2. Create a new service.
Let’s type into our console the following:
ng g service D3/d3-visualization
Edit the service with the following lines of code:
private dataUrl = “https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered_comma.csv”;
fetchData(): Observable<{ date: Date; value: number }[]> { |
We are getting this data from https://www.d3-graph-gallery.com/line which is a community-supported gallery with different examples of visualizations.
We are using ‘d3.csv’ to convert our response to a standard Javascript object, we wrap this in a ‘from’ function which in turn will convert the promise object returned from d3.csv to an observable. Since the data we receive is formatted as a string, we will need to map it so that our date value is a Date type and our value is a numeric type.
Note: It should be noted that the function d3.csv is imported individually. This is particularly recommended for tree shaking when building your application for a production environment. Either way, this would work if used as d3.csv in which case it should be imported as follows
import * as d3 from ‘d3’
or optimally just
import { csv } from ‘d3’
3. Create visualization components
Let’s use the CLI and create the component that will connect to the service and display the data.
ng g component D3/d3-visualization
This should generate these two files:
- d3-visualization.component.html
- d3-visualization.component.ts
We will also want to create a separate component that will receive the data where we will build the chart using D3 so we should go ahead and generate the component as well.
ng g component D3/charts/line-chart
The result should be these files:
- line-chart.component.ts
- line-chart.component.html
4. Edit d3-visualization.component files
d3-visualization.component.ts should look like this:
export class D3VisualizationComponent implements OnInit, OnDestroy { data$: Observable<any>; private unsubscribe$ = new Subject<void>();constructor(private service: D3VisualizationService) {}ngOnInit(): void { this.data$ = this.service.fetchData().pipe(takeUntil(this.unsubscribe$)); } ngOnDestroy(): void { |
We want to have an observable stream that we can subscribe to and be able to unsubscribe when the component is destroyed.
d3-visualization.component.html should look like this:
<ng-container *ngIf=”(data$ | async) as data”> <app-line-chart [data]=”data”> </app-line-chart> </ng-container> |
5. Edit line-chart.component files
Our line-chart.component.html file should be pretty simple:
<div class=”card”> <div class=”card-header”>Example: Bitcoin Price Over Time</div> <div class=”card-body”> <figure #chartArea></figure> </div> </div> |
Also at this point, we can go back to app.component.html and add the reference to our visualization.
<nav class=”navbar navbar-expand-lg navbar-light bg-light”> <div class=”container”> <a class=”navbar-brand” href=”#”>Angular D3 Example</a> </div> </nav> <div class=”container mt-3″> <app-d3-visualization></app-d3-visualization> </div> |
The fun part begins now that we are going to build our visualization. Let’s edit line-chart.component.ts
This component receives as input our formatted data as such.
@Input() data: { date: any; value: number }[];
We will also need to use the ViewChild decorator to be able to select the container and build the chart inside.
@ViewChild(“chartArea”, { static: true }) chartArea: ElementRef<HTMLElement>;
Next, we declare the following properties:
margin = { top: 10, right: 30, bottom: 30, left: 60 }; width: number; height = 400 – this.margin.top – this.margin.bottom; svg: Selection<any, any, any, any>; x: any; y: any; |
In our ngOnInit method we will call 3 functions:
- setSvgArea
- setAxes
- displayLine
So this should look like this:
ngOnInit(): void { this.setSvgArea(); this.setAxes(); this.displayLine(); } |
The setSvgArea function should look like this:
setSvgArea(): void { this.width = this.chartArea.nativeElement.offsetWidth – this.margin.left – this.margin.right;this.svg = select(this.chartArea.nativeElement) .append(“svg”) .attr(“width”, this.width + this.margin.left + this.margin.right) .attr(“height”, this.height + this.margin.top + this.margin.bottom) .append(“g”) .attr(“transform”, `translate(${this.margin.left}, ${this.margin.top})`); } |
Here we are setting the height of our svg to 400 pixels and the width to be equal to the container.
The setAxes function should look like this:
setAxes(): void { this.x = scaleTime() .domain(extent(this.data, (d) => d.date)) .range([0, this.width]); this.y = scaleLinear() .domain([0, max(this.data, (d) => d.value)]) .range([this.height, 0]);this.svg .append(“g”) .attr(“transform”, `translate(0, ${this.height})`) .call(axisBottom(this.x)); this.svg.append(“g”).call(axisLeft(this.y)); } |
In the above function, we are scaling our dimensions to fit the container we established above. Once this is set up we proceed to display.
Finally, we want to draw the line that will represent our data
displayLine(): void { this.svg .append(“path”) .attr(“class”, “line”) .datum(this.data) .attr(“fill”, “none”) .attr(“stroke”, “steelblue”) .attr(“strokewidth”, 1.5) .attr( “d”, line<{ date: any; value: number }>() .x((d) => this.x(d.date)) .y((d) => this.y(d.value)) ); } |
The result is the following:
In Summary
The structure of the project should look like this
- app
- D3
- charts
- line-chart.component.html
- line-chart.component.ts
- d3-visualization.component.ts
- d3-visualization.service.ts
- charts
- app.component.html
- app.component.ts
- D3
- app.component.html
<nav class=”navbar navbar-expand-lg navbar-light bg-light”> <div class=”container”> <a class=”navbar-brand” href=”#”>Angular D3 Example</a> </div> </nav> <div class=”container mt-3″> <app-d3-visualization></app-d3-visualization> </div> |
- app.component.ts
import { Component } from “@angular/core”;
@Component({ |
- d3-visualization.component.ts
import { Component, OnDestroy, OnInit } from “@angular/core”; import { Observable, Subject } from “rxjs”; import { takeUntil } from “rxjs/operators”; import { D3VisualizationService } from “../D3/d3-visualization.service”;@Component({ selector: “app-d3-visualization”, template: ` <ng-container *ngIf=”(data$ | async) as data”> <app-line-chart [data]=”data”> </app-line-chart> </ng-container> `, styleUrls: [“./d3-visualization.component.css”] }) export class D3VisualizationComponent implements OnInit, OnDestroy { data$: Observable<any>; private unsubscribe$ = new Subject<void>();constructor(private service: D3VisualizationService) {} ngOnInit(): void { ngOnDestroy(): void { |
- d3-visualization.service
import { Inject } from “@angular/core”; import { from, Observable } from “rxjs”; import { csv, timeParse } from “d3”; import { map } from “rxjs/operators”;@Inject({}) export class D3VisualizationService { private dataUrl = “https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered_comma.csv”;fetchData(): Observable<{ date: Date; value: number }[]> { const parseTime = timeParse(“%Y-%m-%d”); return from(csv(this.dataUrl)).pipe( map((res: any[]) => res.map(d => ({ …d, date: parseTime(d.date), value: +d.value })) ) ); } } |
- line-chart.component.html
<div class=”card”> <div class=”card-header”>Example: Bitcoin Price Over Time</div> <div class=”card-body”> <figure #chartArea></figure> </div> </div> |
- line-chart.component.ts
import { Component, ElementRef, Input, OnInit, ViewChild } from “@angular/core”; import { Selection, select, scaleTime, scaleLinear, max, extent, axisBottom, axisLeft, line, } from “d3”;@Component({ selector: “app-line-chart”, templateUrl: “./line-chart.component.html”, styleUrls: [“./line-chart.component.css”], }) export class LineChartComponent implements OnInit { @Input() data: { date: any; value: number }[]; @ViewChild(“chartArea”, { static: true }) chartArea: ElementRef<HTMLElement>;margin = { top: 10, right: 30, bottom: 30, left: 60 }; width: number; height = 400 – this.margin.top – this.margin.bottom; svg: Selection<any, any, any, any>; x: any; y: any; ngOnInit(): void { setSvgArea(): void { this.svg = select(this.chartArea.nativeElement) setAxes(): void { this.svg displayLine(): void { |
If you would like to see the finished example you can check it out in Stackblitz. At this point, we have the chart. The next steps will be adding a tooltip and some interactivity.