main banner

Development

Angular and Data-Driven Documents (D3.js) Part 1: Creating Chart

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 }[]> {
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
}))
)
);
}


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 {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}


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:

Bitcoin price over time

 

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

    • app.component.html

    • app.component.ts

 

1. 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>

 

2. app.component.ts

import { Component } from "@angular/core";

@Component({
selector: "my-app",
templateUrl: "./app.component.html"
})
export class AppComponent {}

 

3. 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 {
this.data$ = this.service.fetchData().pipe(takeUntil(this.unsubscribe$));
}

ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}

 

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

 

5. 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>

 

6. 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 {
this.setSvgArea();
this.setAxes();
this.displayLine();
}

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

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

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


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.

Arturo E.

Amateur Musician, Combat Sports Enthusiast, Philosophy and history lover and a software engineer specialized in Javascript technologies.

Articles