Monday, June 17, 2019

Create a responsive Angular D3 charts

While the landscape of the frameworks available for structure and building web applications is changing every minute, D3js is still admitting to create icons using Javascript. In this tutorial, we'll add a chart D3 to a corner type applications and make the size of the graph.

Create a responsive Angular D3 charts

Creating the Angular project

The first step is to create a new Angular project using the CLI, and to add the d3 library to it:

ng new angular-d3
npm install d3 --save
npm install @types/d3 --save-dev
Next, we will create the component that we will work with:

ng generate component bar-chart
Finally, we will replace the content of ‘src/app/app.component.html’ with the following:

<h1>Bar Chart</h1>
<app-bar-chart></app-bar-chart>

Loading and passing data

In this tutorial, we will use this bar chart from Mike Bostock as the D3 visualization. You can find the data in JSON format here, and we will put it in a new asset file at ‘src/assets/data.json’.

We can also create an interface for the data points, in a new file ‘src/app/data/data.model.ts’:

export interface DataModel {
  letter: string;
  frequency: number;
}
To load this data, we can modify the ‘src/app/app.component.ts’ file like this:
angular-d3-app.component.ts
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { DataModel } from 'src/data/data.model';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  data: Observable<DataModel>;
  constructor(private http: HttpClient) {
    this.data = this.http.get<DataModel>('data/data.json');
  }
}
For HttpClient to work, we need to add HttpClientModule to our App NgModule imports in ‘src/app/app.module.ts’.

Finally, we can pass the data to our chart component by modifying ‘src/app/app.component.html’:

<h1>Bar Chart</h1>
<app-bar-chart [data]=”data | async”></app-bar-chart>
Integrating the D3 chart
Let’s replace the content of ‘src/app/bar-char/bar-chart.component.html’ with:

<div #chart id="chart"></div>
As you can see, our component will be code-driven, with nothing in the template except a div, which will serve as our container. The chart size will be inferred from the size of this element, which will be helpful in making the SVG react like a normal html node. Here is the main part of the code, the ‘src/app/bar-chart/bar-chart.component.ts’ file:

angular-d3-bar-chart.component.ts
import { Component, ElementRef, Input, OnChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import * as d3 from 'd3';
import { DataModel } from 'src/app/data/data.model';
@Component({
  selector: 'app-bar-chart',
  encapsulation: ViewEncapsulation.None,
  templateUrl: './bar-chart.component.html',
  styleUrls: ['./bar-chart.component.scss']
})
export class BarChartComponent implements OnChanges {
  @ViewChild('chart')
  private chartContainer: ElementRef;
  @Input()
  data: DataModel[];
  margin = {top: 20, right: 20, bottom: 30, left: 40};
  constructor() { }
  ngOnChanges(): void {
    if (!this.data) { return; }
    this.createChart();
  }
  private createChart(): void {
    d3.select('svg').remove();
    const element = this.chartContainer.nativeElement;
    const data = this.data;
    const svg = d3.select(element).append('svg')
        .attr('width', element.offsetWidth)
        .attr('height', element.offsetHeight);
    const contentWidth = element.offsetWidth - this.margin.left - this.margin.right;
    const contentHeight = element.offsetHeight - this.margin.top - this.margin.bottom;

    const x = d3
      .scaleBand()
      .rangeRound([0, contentWidth])
      .padding(0.1)
      .domain(data.map(d => d.letter));
    const y = d3
      .scaleLinear()
      .rangeRound([contentHeight, 0])
      .domain([0, d3.max(data, d => d.frequency)]);
    const g = svg.append('g')
      .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
    g.append('g')
      .attr('class', 'axis axis--x')
      .attr('transform', 'translate(0,' + contentHeight + ')')
      .call(d3.axisBottom(x));
    g.append('g')
      .attr('class', 'axis axis--y')
      .call(d3.axisLeft(y).ticks(10, '%'))
      .append('text')
        .attr('transform', 'rotate(-90)')
        .attr('y', 6)
        .attr('dy', '0.71em')
        .attr('text-anchor', 'end')
        .text('Frequency');
    g.selectAll('.bar')
      .data(data)
      .enter().append('rect')
        .attr('class', 'bar')
        .attr('x', d => x(d.letter))
        .attr('y', d => y(d.frequency))
        .attr('width', x.bandwidth())
        .attr('height', d => contentHeight - y(d.frequency));
  }
}
This file is mostly a slightly modified version of Mike Bostock’s snippet, but instead of having a static height and width, we use the div element’s height and width. We use the data that is passed through an input property instead of getting it from the file, which separates responsibility. In the component declaration, we changed the ViewEncapsulation, since the dynamic modifications to the DOM by D3 don’t play well with the default Angular styling. Without this modification, the styles aren’t applied.

We can add some styling by adding the following to the renamed ‘src/app/bar-chart/bar-chart.component.scss’ file:

angular-d3-bar-chart.component.scss
app-bar-chart {
    .bar {
        fill: steelblue;
    }
 
    .bar:hover {
        fill: brown;
    }
 
    .axis--x path {
        display: none;
    }
}
Now, when you run ‘ng serve’, you should see the bar chart. You can also notice that it is taking up the whole page. If you resize the page and refresh, you will notice that the chart changes its size accordingly.

Using the component’s width and height
A nice tweak would be to use the width and size of the component, so we can style the component from its parent. We can accomplish this by modifying the ‘src/app/bar-chart/bar-chart.component.scss‘ file like this:

app-bar-chart {
    #chart {
        height: inherit;
        width: inherit;
        .bar {
            fill: steelblue;
        }
     
        .bar:hover {
            fill: brown;
        }
     
        .axis--x path {
            display: none;
        }
    }
}
An example use case would be to make the chart take 50% of its parent width. This is easily done by going to app.component.css and adding the following:

app-bar-chart {
  width: 50%;
}
This is especially useful when using grid systems, where the specific width of a chart in a column varies for almost every screen.

Resizing dynamically
The last tweak that we are going to make is to recreate the chart when the window is resized. This can cause a hit to performance if your chart has a lot of data, but we can manage this by throttling (which won’t be explained in this post series). This should be a relatively unfrequent event anyway.

To resize dynamically, we can modify the file ‘src/app/bar-chart/bar-chart.component.html’ like such:

<div #chart id=”chart” (window:resize)=”onResize($event)”></div>
And we can add the following method to the file ‘src/app/bar-chart/bar-chart.component.ts’ :
onResize() {
  this.createChart();
}
Conclusion
Integrating D3 with Angular can be extremely powerful, as D3 allows us to create amazing visualizations and Angular provides the framework to create everything else related to a web application in a scalable way. This is an example of the power of both libraries combined, as we find ourselves with a chart that can be reused easily, with the responsibility of displaying data completely isolated while still allowing us to style it and position it from its parent container. We do need to be careful when integrating the librairies since they are both modifying the DOM and can possibly alter the expected behavior. The ViewEncapsulation that is not working properly is a great example of this.

No comments:

Post a Comment