Custom chart creation with canvas and Angular

Today, I'd like to show you how you can create a reusable chart using canvas HTML element and Angular.

We are going to develop the following component :


As you can see we have two series of data.

The component will be used as follow:

<custom-chart [data]="datasets"></custom-chart>

Let's first create the service to set up the canvas 2D context and draw our chart.

Chart Service


We are going to create a service to set up our environment and draw on the canvas. 

import {Injectable, ElementRef} from '@angular/core';
export class GroupData {
label: string;
left: number;
right: number;
}
export class ComparisonData {
labels: string[];
datasets: GroupData[];
}
@Injectable()
export class ChartService {
constructor() {}
drawBar(ctx: CanvasRenderingContext2D, x: number, y: number,
color: string, width: number, reverse: boolean) {
let labelX: number;
let barX: number;
if (reverse) {
barX = 100 - width + 40;
labelX = barX - 40;
} else {
barX = 0;
labelX = width + 15;
}
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = color;
ctx.font = "normal 12px Arial";
ctx.fillText(`${width} %`, labelX, 24);
ctx.fillRect(barX, 0, width, 40);
ctx.restore();
}
drawComparisonChart(canvasRef: ElementRef, data: ComparisonData) {
//Serie A Total
let leftTotalValue = 0;
//Serie B Total
let rightTotalValue = 0;
//Canvas width
let canvasWidth: number = 300;
//Canvas height
let canvasHeight: number = 0;
//bar height
let height: number = 40;
//margin between bar
let margin: number = 25;
//Set totals and canvas height
data.datasets.forEach((element) => {
let left = element.left;
leftTotalValue += left;
let right = element.right;
rightTotalValue += right;
canvasHeight += height + margin;
});
//add space for legends
canvasHeight += 80;
//Set canvas environment
let canvas: HTMLCanvasElement = canvasRef.nativeElement;
canvas.height = canvasHeight;
canvas.width = canvasWidth;
//get canvas context for drawing
let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
//safe tests
if (leftTotalValue > 0 && rightTotalValue > 0 && data.labels.length === 2) {
let y: number;
data.datasets.forEach((element, index) => {
//coordinate for translation
y = (index * height) + (margin * (index + 1));
//create label
ctx.save();
ctx.translate(143, y - 28);
ctx.fillStyle = '#90CAF9';
ctx.font = "normal 12px Arial";
ctx.fillText(element.label, 0, 24);
ctx.restore();
//left serie
let leftWidth = Math.round((element.left / leftTotalValue) * 100);
this.drawBar(ctx, 0, y, '#90CAF9', leftWidth, true);
//right serie
let rightWidth = Math.round((element.right / rightTotalValue) * 100);
this.drawBar(ctx, 143, y, '#80DEEA', rightWidth, false);
});
//legends
ctx.save();
ctx.translate(0, y + 50);
ctx.fillStyle = '#90CAF9';
ctx.font = "normal 14px Arial";
let leftLabel: TextMetrics = ctx.measureText(data.labels[0]);
let leftLabelX: number = 140 - (leftLabel.width);
ctx.fillText(data.labels[0], leftLabelX, 15);
ctx.fillStyle = '#80DEEA';
ctx.fillText(data.labels[1], 143, 15);
ctx.restore();
}
}
}
view raw ChartService hosted with ❤ by GitHub
The data format should be:

{
labels: ['Offer', 'Demand'],
datasets: [
{
label: 'Asia',
left: 678,
right: 3277
},
{
label: 'Europe',
left: 0,
right: 2900
},
{
label: 'Africa',
left: 0,
right: 1190
},
{
label: 'Oceania',
left: 342,
right: 6339
},
{
label: 'North America',
left: 0,
right: 6089
},
{
label: 'South America',
left: 456,
right: 8084
}
]
}
view raw chartFormatData hosted with ❤ by GitHub

Chart Component

We access the canvas reference using the @ViewChild annotation. We will use @Input annotation for data binding.

import {Component, OnInit, ViewChild, Input, ElementRef} from '@angular/core';
import {ChartService, ComparisonData} from '../../core/services/chart.service';
@Component({
selector: 'custom-chart',
template: '<canvas #myCanvas></canvas>'
})
export class CustomComponent implements OnInit {
@ViewChild('myCanvas') canvasRef: ElementRef;
@Input() data: ComparisonData;
constructor(
private chartService: ChartService
) {}
ngOnInit(): void {
this.chartService.drawComparisonChart(this.canvasRef, this.data);
}
}
view raw ChartComponent hosted with ❤ by GitHub

We can easily add more functionality to our component, but for demonstration purpose it's enough.


Comments

Post a Comment

Popular posts from this blog

Spring JPA : Using Specification with Projection

Chip input using Reactive Form