Build a simple line chart with D3.js in Angular

Babette Landmesser
Nov 15, 2020 · 6 min read
Image for post
Image for post

In this post I’m going to show you how to integrate D3.js into your Angular application and how to create a simple styled line chart. Also, we’re covering changes of the chart data and on responsiveness.

I assume you already have an Angular app running on your local machine.

Pre-requisites

Install D3.js as a dependency and as we’re working with Typescript, install the d3 types as well:

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

Next, create a component for your chart via Angular CLI:

ng generate component line-chart

Within the Typescript file of this component, we’re going to import d3:

import * as d3 from 'd3'

Add the above JSON to your app.component.ts:

public data = [
{
value: 20,
date: '2020-05-12T12:19:00+00:00'
},
...
];

We want to pass it as an input to our line chart component.

Line Chart Component

In the line-chart.component.html we only need to add a div:

<div class=„linechart”></div>

In the line-chart.component.ts the first line to add is the following:

@Input() public data: { value: number, date: string }[];

This line enables passing the data from the parent component to the line-chart component. Next, in the parent component we can use the line chart component:

<app-line-chart [data]=”data”></app-line-chart>

After that, go back to the line-chart.component.ts and add the following basics:

private width = 700;
private height = 700;
private margin = 50;
public svg;
public svgInner;
public yScale;
public xScale;
public xAxis;
public yAxis;
public lineGroup;
public constructor(public chartElem: ElementRef) {
}

The chart should be 700x700px large and have a margin of 50px. To make it easier for changes later, I usually set these values to component variables. This will also help when adding responsiveness as it depends on the width of the chart.

All other component attributes are also needed for the SVG creation, the axes, and lines. Here again, using component attributes will be useful for responsiveness and also for reacting to data changes.

Drawing the chart

I’m usually splitting up the code for charts into two functions: One function to initialize all elements that I am going to need and one function to actually use and place the elements. This makes it easier in the end to enable responsiveness in which the chart needs to react on browser width sizes. The second function therefore changes all values that are dependent on the width of the chart: all elements that have to do with the x-Axis but also the lines and the complete SVG.

Initializing the chart elements

Create a function called „initializeChart“. At first, the SVG has to be created inside our element reference:

this.svg = d3
.select(this.chartElem.nativeElement)
.select(‘.linechart’)
.append(‘svg’)
.attr(‘height’, this.height);

Here is one of the Angular specific changes in comparison to „simple“ D3 projects: First, d3 has to select the element reference with its nativeElement and then select the element with my classname. It would also be possible to work with @ViewChild and a template reference here but I prefer the element reference.

Either way, be aware that if you only select an element with a class name — which would also work — there will later be issues if you want to reuse the component, having two instances in one parent component. By using the element or template reference you’re ensuring that all code is applied to this exact instance.

Inside the SVG element I want to have a group which adds the margin to my chart:

this.svgInner = this.svg
.append(‘g’)
.style(‘transform’, ‘translate(‘ + this.margin + ‘px, ‘ + this.margin + ‘px)’);

Now, we’re digging deeper into the SVG creation and also into our passed data. As you can see in the gist, I’m using a date and a value for each entry. I want to have the dates displayed on the x-axis and the values on the y-axis. So we need to add two scales, xScale and yScale, for being able to display all our datasets within our width and height of the SVG.

this.yScale = d3
.scaleLinear()
.domain([d3.max(this.data, d => d.value) + 1, d3.min(this.data, d => d.value) — 1])
.range([0, this.height — 2 * this.margin]);
this.xScale = d3.scaleTime().domain(d3.extent(this.data, d => new Date(d.date)));

Here you can see the difference between using a time scale and a simple linear scale. For the linear scale, I use the d3 helper functions max() and min() to get the lowest and highest value of my datasets. I do add and remove one to get a small „padding“ — meaning the highest and lowest value in the line are not exactly placed on the top or bottom of my SVG.

As the x-axis depends on the width of the element, we’re here unable to set the range which sets the scaling to the container width.

Next, we’re going to add the axes.

this.yAxis = this.svgInner
.append(‘g’)
.attr(‘id’, ‘y-axis’)
.style(‘transform’, ‘translate(‘ + this.margin + ‘px, 0)‘);
this.xAxis = this.svgInner
.append(‘g’)
.attr(‘id’, ‘x-axis’)
.style(‘transform’, ‘translate(0, ‘ + (this.height — 2 * this.margin) + ‘px)’);

Those of you who are already familiar with D3.js will recognize that I don’t call the axes with their scales. As they depend on the screen sizes, this is something I’m going to add later when filling the chart. These initializations here ensure that the axes with their containers are already available and I don’t need to instantiate them on every resize.

The same applies to the line:

this.lineGroup = this.svgInner
.append('g')
.append('path')
.attr('id', 'line')
.style('fill', 'none')
.style('stroke', 'red')
.style('stroke-width', '2px');

That’s all for initializing the SVG chart. Let’s head over to applying data to it.

Apply data to the SVG

Create a function named „drawChart“. Keep in mind, that this function is going to be our function to react to resizing events. That’s why everything in there relies on the element width. So first of all, we’re setting the new width, calculated from our element reference:

this.width = this.chartElem.nativeElement.getBoundingClientRect().width;this.svg.attr(‘width’, this.width);

Also, we’re setting the range for the x-axis and filling both axes with their scales and hence the data:

this.xScale.range([this.margin, this.width — 2 * this.margin]);const xAxis = d3
.axisBottom(this.xScale)
.ticks(10)
.tickFormat(d3.timeFormat('%m / %Y'));
this.xAxis.call(xAxis);const yAxis = d3
.axisRight(this.yScale);
this.yAxis.call(yAxis);

The last thing we’re going to do is to add our line. We first generate a line path with a monotone curve. Also, we need to map our data by creating a two-level array in which every entry contains the X and Y value:

const line = d3
.line()
.x(d => d[0])
.y(d => d[1])
.curve(d3.curveMonotoneX);
const points: [number, number][] = this.data.map(
d => [this.xScale(new Date(d.date)), this.yScale(d.value)]
);
this.lineGroup.attr(‘d’, line(points));

Then, we simply set the d attribute on the line path we created in our initializer.

Make the chart responsive and listen to changes

The very last part of this tutorial includes the listening to changes and the listener for resizing.

We want to check that whenever the data from outside is changing, the chart gets updated. Angular provides the lifecycle hook „OnChanges“ which we’re going to use. For the resizing issue, we’re always listening to window resizing and also draw the chart again. Hence, we’re able to cover both cases in our ngOnChanges function:

public ngOnChanges(changes): void {
if (changes.hasOwnProperty(‘data’) && this.data) {
this.initializeChart();
this.drawChart();
window.addEventListener(‘resize’, () => this.drawChart());
}
}

And that’s it. 🥳 Your chart should look similar to this:

Image for post
Image for post
The result of the generated chart.

Your complete component code should be like this:

Weekly Webtips

Explore the world of web technologies through a series of tutorials

Sign up for 💌 Weekly Newsletter

By Weekly Webtips

Get the latest news on the world of web technologies with a series of tutorial Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Babette Landmesser

Written by

Frontend / Angular Developer from Mainz, Germany. https://babettelandmesser.de

Weekly Webtips

Explore the world of web technologies through a series of tutorials

Babette Landmesser

Written by

Frontend / Angular Developer from Mainz, Germany. https://babettelandmesser.de

Weekly Webtips

Explore the world of web technologies through a series of tutorials

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store