Developing and Using Angular Libraries
A developer gives a tutorial on how to create, configure, and use an Angular library, including all the TypeScript code you will need to get going.
Join the DZone community and get the full member experience.
Join For FreeIn the AngularAndSpring project charts are displayed. To display the charts I have developed the ngx-simple-charts library. The design goal is to be D3.js based, have a typesafe Angular API, and be so simple and small that it can be migrated fast to new Angular versions. Currently the library supports line charts, bar charts might be supported in the future.
Creating and Configuring the Angular Library
The Angular CLI can generate a library shell with this command: ng generate library <library_name>
In the library shell you get a Module, a Component, and a Service. The library API is defined in the public-api file.
To add the D3 charts, the libraries and typings need to be added in the package.json:
x
"dependencies": {
"d3-array": "^2.9.1",
"d3-axis": "^2.0.0",
"d3-brush": "^2.1.0",
"d3-color": "^2.0.0",
"d3-format": "^2.0.0",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"d3-selection": "^2.0.0",
"d3-shape": "^2.0.0",
"d3-time-format": "^3.0.0",
"d3-transition": "^2.0.0"
},
"devDependencies": {
"@types/d3-array": "^2.8.0",
"@types/d3-brush": "^2.1.0",
"@types/d3-color": "^2.0.1",
"@types/d3-format": "^2.0.0",
"@types/d3-interpolate": "^2.0.0",
"@types/d3-scale": "^3.2.2",
"@types/d3-selection": "^2.0.0",
"@types/d3-shape": "^2.0.0",
"@types/d3-time-format": "^3.0.0",
"@types/d3-transition": "^2.0.0",
"@types/d3-axis": "^2.0.0"
}
The scripts part of the package.json has these additions:
xxxxxxxxxx
"scripts": {
"build-watch": "ng build --watch",
"build-prod": "ng build --configuration=production"
}
The 'build-watch' script rebuilds the library on change. That helps development.
The 'build-prod' script builds the library with production settings that are needed to publish it on npm. According to the Angular team, libraries should not be published with Ivy enabled.
In the library folder, the package.json needs to get the library dependencies:
xxxxxxxxxx
"peerDependencies": {
"@angular/common": "^11.1.0",
"@angular/core": "^11.1.0"
},
"dependencies": {
"d3-array": "^2.9.1",
"d3-axis": "^2.0.0",
"d3-brush": "^2.1.0",
"d3-color": "^2.0.0",
"d3-format": "^2.0.0",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"d3-selection": "^2.0.0",
"d3-shape": "^2.0.0",
"d3-time-format": "^3.0.0",
"d3-transition": "^2.0.0",
"tslib": "^2.0.0"
}
The 'peerDependencies' are expected from the project that uses the library.
The 'dependencies' are used to build the library.
The ng-package.json has the umd module config and the whitelisted dependencies:
xxxxxxxxxx
"lib": {
"entryFile": "src/public-api.ts",
"umdModuleIds": {
"d3-array": "d3-array",
"d3-brush": "d3-brush",
"d3-color": "d3-color",
"d3-format": "d3-format",
"d3-interpolate": "d3-interpolate",
"d3-scale": "d3-scale",
"d3-selection": "d3-selection",
"d3-shape": "d3-shape",
"d3-time-format": "d3-time-format",
"d3-transition": "d3-transition",
"d3-axis": "d3-axis"
}
},
"whitelistedNonPeerDependencies": ["d3"]
If the 'umdModuleIds' are not provided, Angular will guess at them and the 'd3' dependencies need to be whitelisted.
Implementing the Charts in the Library
The component and the service have been moved to their own folders. To add the charts to the component the template has been updated:
xxxxxxxxxx
<div class="d3-chart-wrapper">
<svg #svgchart class="d3-chart"></svg>
</div>
The CSS classes offer some styling options and make the chart use all the space that is available in the div.
The SVG tag is the host element with the 'svgchart' reference variable of the chart that is generated with the D3 library.
The SCSS file makes the chart file the available space and provides some basic styling:
xxxxxxxxxx
.d3-chart {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.d3-chart-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
::ng-deep .line {
fill: none;
stroke: #1E90FF;
stroke-width: 3;
}
...
The 'd3-chart' and 'd3-chart-wrapper' classes can be used to change the chart size.
The 'line' class is used to set the line style.
The chart component is setup like this:
@Component({
selector: 'sc-line-chart',
templateUrl: './sc-line-chart.component.html',
styleUrls: ['./sc-line-chart.component.scss'],
encapsulation: ViewEncapsulation.Emulated,
})
export class ScLineChartComponent implements AfterViewInit, OnChanges {
@ViewChild("svgchart", {static: true})
private chartContainer!: ElementRef;
private d3Svg!: Selection<ContainerElement, ChartPoint, HTMLElement, any>;
@Input()
private chartPoints: ChartPoints[] = [];
private gAttribute!: Selection<SVGGElement, ChartPoint, HTMLElement, any>;
private gxAttribute!: Selection<SVGGElement, ChartPoint, HTMLElement, any>;
private gyAttribute!: Selection<SVGGElement, ChartPoint, HTMLElement, any>;
private gPathAttribute!: Selection<SVGPathElement, ChartPoint, HTMLElement, any>;
ngAfterViewInit(): void {
this.d3Svg = select<ContainerElement, ChartPoint>(this.chartContainer.nativeElement);
this.gxAttribute = this.d3Svg.append('g');
this.gxAttribute = this.gxAttribute.attr('class', 'x axis');
this.gyAttribute = this.d3Svg.append('g');
this.gyAttribute = this.gyAttribute.attr('class', 'y axis');
this.gPathAttribute = this.d3Svg.append('path');
this.updateChart();
}
ngOnChanges(changes: SimpleChanges): void {
if (!!changes['chartPoints'] && !changes['chartPoints'].isFirstChange()) {
this.updateChart();
}
}
In lines 1-6, the 'sc-line-chart' component is defined.
In line 7, the interfaces AfterViewInit
and OnChanges
are added.
In lines 8-9, the SVG element ref is added with the ViewChild
annotation that references the template variable. The 'static' option has to be set to get SVG element ref.
In line 10, the d3Svg selection is defined to get the D3 representation of the element ref.
In lines 11-12, the input component parameter chartPoints
is defined. That is expected to contain the chart values in the ChartPoints format.
In lines 13-16, the D3 attributes for the x axis, the y axis, and the chart path are defined.
In lines 18-19, the method ngAfterViewInit
initializes the d3svg
property with the SVG element ref. The ngAfterViewInit
method needs to be used because the ViewChild
annotated value has to be availiable.
In lines 21-25, the x axis and the y axis are set with its CSS classes and the path is initialized.
In line 27, updateChart
is called to render the chart.
In line 30, the ngChanges
method is used to get notified of updates of the component parameters.
In lines 31-33, the method checks for changes to the chartPoints
component parameter and filters out the first change. It calls then the updateChart
method to render the chart.
To render the charts the updateChanges
method is used:
x
private updateChart(): void {
const contentWidth = isNaN(parseInt(this.d3Svg.style('width')
.replace(/[^0-9\.]+/g, ''), 10)) ?
0 : parseInt(this.d3Svg.style('width')
.replace(/[^0-9\.]+/g, ''), 10);
const contentHeight = isNaN(parseInt(this.d3Svg.style('height')
.replace(/[^0-9\.]+/g, ''), 10)) ?
0 : parseInt(this.d3Svg.style('height')
.replace(/[^0-9\.]+/g, ''), 10);
this.d3Svg.attr("viewBox", [0, 0, contentWidth, contentHeight] as any)
if (contentHeight < 1 || contentWidth < 1 || !this.chartPoints
|| this.chartPoints.length === 0 || !this.chartPoints[0].chartPointList
|| this.chartPoints[0].chartPointList.length === 0) {
console.log(`contentHeight: ${contentHeight} contentWidth: ${contentWidth} chartPoints: ${this.chartPoints.length}`);
return;
}
//console.log(`chartPoints: ${this.chartPoints.length} chartPointList: ${this.chartPoints[0].chartPointList.length}`);
let xScale: ScaleTime<number, number, never> | ScaleLinear<number, number, never>;
if (this.chartPoints[0].chartPointList[0].x instanceof Date) {
xScale = scaleTime()
.domain(extent(this.chartPoints[0].chartPointList, p => p.x as Date) as [Date, Date])
.range([0, contentWidth - this.chartPoints[0].yScaleWidth])
} else {
xScale = scaleLinear()
.domain([0, this.chartPoints[0].chartPointList.length - 1]).nice()
.range([0, contentWidth - this.chartPoints[0].yScaleWidth]);
}
//console.log(xScale);
const yScale = scaleLinear()
.domain(extent<ChartPoint, number>(this.chartPoints[0].chartPointList, p => p.y) as [number, number]).nice()
.range([contentHeight - this.chartPoints[0].xScaleHeight, 0]);
const myLine = line()
.defined(p => (p as unknown as ChartPoint).y !== null && !isNaN((p as unknown as ChartPoint).y))
.x((p,i) => xScale((p as unknown as ChartPoint).x instanceof Date ?
(p as unknown as ChartPoint).x as Date: i))
.y((p) => yScale((p as unknown as ChartPoint).y))
.curve((p) => curveMonotoneX(p));
this.gPathAttribute.datum(this.chartPoints[0].chartPointList)
.attr('transform', 'translate('+ this.chartPoints[0].yScaleWidth +', 0)')
.attr('class', 'line').attr('d', myLine as any);
this.gxAttribute
.attr("transform", "translate(" + (this.chartPoints[0].yScaleWidth) + ","
+ (contentHeight - this.chartPoints[0].xScaleHeight) + ")")
.call(axisBottom(xScale));
this.gyAttribute
.attr("transform", "translate(" + (this.chartPoints[0].yScaleWidth) + "," + 0 + ")")
.call(axisLeft(yScale));
}
In lines 2-9, the width and the height are read out from the SVG element.
In line 10, the viewbox is set for the SVG element.
In lines 11-16, the input values are validated.
In lines 19-28, the x scale is generated. It expects either a Date or a String value.
In lines 32-34, the y scale is generated. It expects a number value.
In lines 36-41, the chart line is generated. It supports the data types of the scales and filters out invalid values. The space between data points is filled by the curveMonotoneX(...)
function.
In lines 44-46, the chart line is added to the SVG element with the line class for styling.
In lines 47-51, the x scale is added to the SVG element.
In lines 52-55, the y scale is added to the SVG element.
The Library Interface
To use the library it needs a typesafe interface. That is defined in the Module and the public-api file:
xxxxxxxxxx
export * from './lib/service/ngx-simple-charts.service';
export * from './lib/sc-line-chart/sc-line-chart.component';
export * from './lib/ngx-simple-charts.module';
export * from './lib/model/chart-points';
In lines 1-4, the typescript files that should be exported are declared. They contain the public interface of the library.
The Angular library module file:
xxxxxxxxxx
@NgModule({
declarations: [ScLineChartComponent],
imports: [CommonModule],
exports: [ScLineChartComponent],
providers: [NgxSimpleChartsService]
})
export class NgxSimpleChartsModule { }
In lines 1-6, the Angular Module of the library is defined. It has the chart component, a service, and imports the Angular CommonModule that is needed in the chart component. The chart component is exported to be used by Angular projects.
The TypeScript types for the chart component are in the chart-point file:
xxxxxxxxxx
export interface ChartPoint{
y: number;
x: string | Date;
}
export interface ChartPoints {
name: string;
yScaleWidth: number;
xScaleHeight: number;
chartPointList: ChartPoint[];
}
The ChartPoints
interface defines the chart name, the scale widths, and the chartPointList
array.
Using the Library
The AngularAndSpring project uses the ngx-simple-charts library. During the development of the library the npm link feature can be used. The ngx-simple-charts/dist/ngx-simple-charts/ directory can be linked into the project that uses the library. Then, 'npm run build-watch' rebuilds the library if the code is changed.
Add the library to the package.json:
xxxxxxxxxx
"dependencies": {
"ngx-simple-charts": "^0.0.3"
}
Install the library.
The library module is added to the details.module:
xxxxxxxxxx
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatToolbarModule,
MatRadioModule,
MatButtonModule,
DetailsRoutingModule,
NgxSimpleChartsModule,
],
declarations: [
IbdetailComponent,
CbdetailComponent,
BsdetailComponent,
BfdetailComponent
]
})
export class DetailsModule { }
The imported Modules now include NgxSimpleChartsModule in line 10. The module declares the detail components to show the data and charts of the currencies.
The AngularAndSpring project has four components that display charts. The components have the common base class detail-base:
xxxxxxxxxx
export abstract class DetailBase {
chartPoints: ChartPoints[] = [];
utils = new CommonUtils();
currPair = '';
timeframe = this.utils.timeframes[0];
readonly yScaleWidth = 50;
readonly xScaleHeight = 20;
constructor(protected locale: string) {}
protected updateChartData(values: Tuple<string, number>[]): void {
const myChartPoint = values.map(myCP => ({x: new Date(myCP.A), y: myCP.B} as ChartPoint));
this.chartPoints = [{name: this.currPair, chartPointList: myChartPoint,
yScaleWidth: this.yScaleWidth, xScaleHeight: this.xScaleHeight} as ChartPoints];
//console.log(this.chartPoints);
}
}
In lines 1-7, the abstract class with the common properties is defined.
In line 11, the method updateChartData
is defined with its common interface of Tuples.
In line 12, the chart points in the tuples are mapped in the ChartPoint
interface array.
In lines 13-14, an array of the ChartPoints
interface is created for the ngs-simple-charts library. The library expects a ChartPoints
array to be able to support multi-line charts in the future.
The 'sc-line-chart' component is used in the template bfdetail.component.html:
xxxxxxxxxx
<div #chartContainer class="chart-container">
<sc-line-chart [chartPoints]="chartPoints"></sc-line-chart>
</div>
In line 2, the sc-line-chart
component of the library is used and the chart points are provided by the DetailBase
class that the component class BfdetailComponent extends.
Conclusion
Creating and publishing the ngx-simple-charts library took some time. The Angular CLI helped a lot by generating a base. The npm link feature help in the development process. The implementation is very close to the examples of D3.js. There are examples of other Angular libraries that can be used for ideas. With all that help it was possible to build this library quite fast and easier than expected. A thank you to the Angular Team for the user friendly library architecture and CLI support.
The Future of ngx-simple-charts
The library enables developer to quickly update to new Angular versions. It is intended that it will stay small and with few features. In the future there might be more features, but only if that does not interfere with the fast updates goal. There are no resources to make it a fully featured chart library.
Opinions expressed by DZone contributors are their own.
Comments