Easily Create Charts In Angular 13 with Dynamic Data

Introduction 

A chart is a graphical representation for data visualization, in which the data is represented by symbols such as slices in a pie chart, lines in a line chart, or bars in a bar chart. A chart can stand for tabular numeric data, functions or some kinds of quality structure and supplies different info. 

Charts are often used to ease understanding of massive quantities of data and the relationships between parts of the data. Charts can usually be read more quickly than the raw data. Certain types of charts are more useful for presenting a given data set than others. For example, data that presents percentages in separate groups (such as satisfied, not satisfied, unsure) are often displayed in a pie chart, but maybe more easily understood when presented in a horizontal bar chart. On the other hand, data that stands for numbers that change over a period (such as annual revenue from 2002 to 2022) might be best shown in a line chart. 

We can easily create charts in Angular using Chart.js.  

Chart.js is a popular open-source library used in Angular to create several types of charts.  

You can get more information from https://www.chartjs.org/

Ng2-charts is an Angular directive for Chart.js. We can use this directive to create attractive charts in Angular.  

Ng2-charts is developed by Valor software company.

You can get more information from https://valor-software.com/ng2-charts/  

In this post, we will see how to create diverse types of charts in Angular 13 using Chart.js and Ng2-charts libraries. We will be creating Pie, Doughnut, Polar Area, Radar, Bar, and Line charts. We will load data dynamically to the charts. We will use C# Corner authors articles/blogs details as dynamic data for charts. So that we can see each author’s category-wise post count in the chart.  

I have already created a .NET 6.0 Web API application to get C# Corner author data and total post details using web scraping. I have used HtmlAgilityPack library to scrap the data in .NET 6.0. You can refer to the article below for more information.  

Easily do Web Scraping in .NET Core 6.0

Modify existing .NET 6.0 Web API to get C# Corner Author details 

We can use the .NET 6.0 Web API project that we created earlier and make the changes below. 

We can allow CORS (Cross Origin Resource Sharing) using the code changes below in Program class. So that Angular client application can communicate with Web API without any issues.  

Program.cs 

// Partial Code for Program.cs

var MyAllowedOrigins = "_myAllowedOrigins";

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowedOrigins,
                          builder =>
                          {
                              builder.WithOrigins("http://localhost:4200")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
});

var app = builder.Build();

app.UseCors(MyAllowedOrigins);

We can create an Authors model class under Models folder for author details.  

Authors.cs 

namespace Analyitcs.NET6._0.Models
{
    public class Authors
    {
        public string? AuthorId { get; set; }
        public string? Author { get; set; }
        public int Count { get; set; }
    }
}

We can create Category model class under Models folder for author category details. 

Category.cs 

namespace Analyitcs.NET6._0.Models
{
    public class Category
    {
        public string? Name { get; set; }
        public int Count { get; set; }
    }
}

Now we can change the Analytics controller with the code changes below.  

AnalyticsController.cs 

using Analyitcs.NET6._0.Models;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Net;
using System.Xml.Linq;

namespace Analyitcs.NET6._0.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AnalyticsController : ControllerBase
    {
        readonly CultureInfo culture = new("en-US");
        private readonly MyDbContext _dbContext;
        private readonly IConfiguration _configuration;
        public AnalyticsController(MyDbContext context, IConfiguration configuration)
        {
            _dbContext = context;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("CreatePosts/{authorId}")]
        public async Task<bool> CreatePosts(string authorId)
        {
            try
            {
                XDocument doc = XDocument.Load("https://www.c-sharpcorner.com/members/" + authorId + "/rss");
                if (doc == null)
                {
                    return false;
                }
                var entries = from item in doc.Root.Descendants().First(i => i.Name.LocalName == "channel").Elements().Where(i => i.Name.LocalName == "item")
                              select new Feed
                              {
                                  Content = item.Elements().First(i => i.Name.LocalName == "description").Value,
                                  Link = (item.Elements().First(i => i.Name.LocalName == "link").Value).StartsWith("/") ? "https://www.c-sharpcorner.com" + item.Elements().First(i => i.Name.LocalName == "link").Value : item.Elements().First(i => i.Name.LocalName == "link").Value,
                                  PubDate = Convert.ToDateTime(item.Elements().First(i => i.Name.LocalName == "pubDate").Value, culture),
                                  Title = item.Elements().First(i => i.Name.LocalName == "title").Value,
                                  FeedType = (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("blog") ? "Blog" : (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("news") ? "News" : "Article",
                                  Author = item.Elements().First(i => i.Name.LocalName == "author").Value
                              };

                List<Feed> feeds = entries.OrderByDescending(o => o.PubDate).ToList();
                string urlAddress = string.Empty;
                List<ArticleMatrix> articleMatrices = new();
                _ = int.TryParse(_configuration["ParallelTasksCount"], out int parallelTasksCount);

                Parallel.ForEach(feeds, new ParallelOptions { MaxDegreeOfParallelism = parallelTasksCount }, feed =>
                {
                    urlAddress = feed.Link;

                    var httpClient = new HttpClient
                    {
                        BaseAddress = new Uri(urlAddress)
                    };
                    var result = httpClient.GetAsync("").Result;

                    string strData = "";

                    if (result.StatusCode == HttpStatusCode.OK)
                    {
                        strData = result.Content.ReadAsStringAsync().Result;

                        HtmlDocument htmlDocument = new();
                        htmlDocument.LoadHtml(strData);

                        ArticleMatrix articleMatrix = new()
                        {
                            AuthorId = authorId,
                            Author = feed.Author,
                            Type = feed.FeedType,
                            Link = feed.Link,
                            Title = feed.Title,
                            PubDate = feed.PubDate
                        };

                        string category = "Uncategorized";
                        if (htmlDocument.GetElementbyId("ImgCategory") != null)
                        {
                            category = htmlDocument.GetElementbyId("ImgCategory").GetAttributeValue("title", "");
                        }

                        articleMatrix.Category = category;

                        var view = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='ViewCounts']");
                        if (view != null)
                        {
                            articleMatrix.Views = view.InnerText;

                            if (articleMatrix.Views.Contains('m'))
                            {
                                articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000000;
                            }
                            else if (articleMatrix.Views.Contains('k'))
                            {
                                articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000;
                            }
                            else
                            {
                                _ = decimal.TryParse(articleMatrix.Views, out decimal viewCount);
                                articleMatrix.ViewsCount = viewCount;
                            }
                        }
                        else
                        {
                            articleMatrix.ViewsCount = 0;
                        }
                        var like = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='LabelLikeCount']");
                        if (like != null)
                        {
                            _ = int.TryParse(like.InnerText, out int likes);
                            articleMatrix.Likes = likes;
                        }

                        articleMatrices.Add(articleMatrix);
                    }

                });

                _dbContext.ArticleMatrices.RemoveRange(_dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId));

                foreach (ArticleMatrix articleMatrix in articleMatrices)
                {
                    await _dbContext.ArticleMatrices.AddAsync(articleMatrix);
                }

                await _dbContext.SaveChangesAsync();
                return true;
            }
            catch
            {
                return false;
            }
        }

        [HttpGet]
        [Route("GetAuthors")]
        public IQueryable<Authors> GetAuthors()
        {
            return from x in _dbContext.ArticleMatrices.GroupBy(x => x.AuthorId)
                   select new Authors
                   {
                       AuthorId = x.FirstOrDefault().AuthorId,
                       Author = x.FirstOrDefault().Author,
                       Count = x.Count()
                   };
        }

        [HttpGet]
        [Route("GetCategory/{authorId}")]
        public IQueryable<Category> GetCategory(string authorId)
        {
            return from x in _dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId).GroupBy(x => x.Category)
                   select new Category
                   {
                       Name = x.FirstOrDefault().Category,
                       Count = x.Count()
                   };
        }

    }
}

Along with CreatePosts method, we have added two more methods GetAuthors and GetCategory 

We have successfully completed the Web API for getting C# Corner Author posts category details. We have already enabled swagger. If needed, you can check all the API end points using swagger.  

Create Angular 13 application to display Chart 

We must install the client libraries below in our Angular application.  

  • chart.js 
  • ng2-charts 
  • bootstrap 
  • font-awesome

We can use below single npm command to install all these libraries.  

npm install bootstrap chart.js font-awesome ng2-charts 

We can change styles.css file with code changes given below. 

styles.css 

@import "~bootstrap/dist/css/bootstrap.css";  
@import "~font-awesome/css/font-awesome.css";

We have imported style references for bootstrap and font-awesome libraries into the above stylesheet. So that in future we can use the above libraries in any of the Angular components without further individual import statement.  

We can add Web API base URL inside the environment variable. We can use this URL in any of the Angular components easily to connect .NET 6.0 Web API. 

environment.ts 

export const environment = {
  production: false,
  baseUrl: 'http://localhost:5000/api/'
};

We can create a new component with a simple header and footer in our application.  

ng generate c NavMenu 

Modify the html template with the code changes below. 

nav-menu.component.html 

<header>
    <nav class='navbar-expand-sm navbar-toggleable-sm navbar-light bg-
white border-bottom box-shadow mb-3 align-center'>
        <div class="container">
            <a class="nav-link title">Charts in Angular 13
                <img width="50" alt="Angular Logo"
                    src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
                with dynamic data</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse"
                aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
        </div>
    </nav>
</header>
<footer>
    <nav class="navbar navbar-light bg-
white mt-5 fixed-bottom">
        <div class="navbar-expand m-auto navbar-text">
            Developed with <i class="fa fa-heart"></i> by <b>Sarathlal
                Saseendran</b>
        </div>
    </nav>
</footer>

Modify the stylesheet with the code changes below. 

nav-menu.component.css 

.fa-heart {
    color:
        hotpink;
}

.align-center {
    text-align: center
}

.title {
    color: black;
    font-weight: bold;
    font-size: large;
}

We are using Angular reactive forms in this application. We must add the modules below in AppModule class file. 

  • HttpClientModule, 
  • FormsModule, 
  • ReactiveFormsModule, 
  • NgChartsModule 

FormsModule and ReactiveFormsModule are used for reactive forms and NgChartsModule is used for creating Ng2charts directive for our chart.  

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { NgChartsModule } from 'ng2-charts';
import { NavMenuComponent } from './nav-menu/nav-menu.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    NgChartsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We can change the App component with the code changes below. 

Modify the component class file. 

app.component.cs 

import { Component } from '@angular/core';
import { ChartData, ChartOptions } from 'chart.js';
import { HttpClient } from '@angular/common/http';
import { FormGroup, FormBuilder } from '@angular/forms';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private http: HttpClient, private fb: FormBuilder) { }

  chartData: ChartData<'pie'> = {
    labels: [],
    datasets: [
      {
        data: [],
      }
    ]
  };

  chartOptions: ChartOptions = {
    responsive: true,
    plugins: {
      title: {
        display: true,
        text: '',
      },
      legend: {
        display: false
      },
    },
  };

  authors: Author[] = [];
  authorForm!: FormGroup;
  showLoader!: boolean;
  totalPosts!: number;
  categories: string[] = [];
  counts: number[] = [];

  private url = environment.baseUrl + 'analytics';

  ngOnInit(): void {
    this.authorForm = this.fb.group({
      authorId: '',
      chartType: 'pie',
      author: null,
      category: '',
      showLegend: false
    });
    this.showAuthors();
  }

  showAuthors() {
    this.showLoader = true;
    this.http.get<Author[]>(this.url + '/getauthors')
      .subscribe({
        next: (result) => {
          this.authors = result;
          this.showLoader = false;
        },
        error: (err) => {
          console.error(err);
          this.showLoader = false;
        },
        complete: () => console.info('Get authors completed')
      });
  }

  populateData() {
    if (!this.authorForm.value.authorId) {
      alert('Please give a valid Author Id');
      return;
    }
    this.categories = [];
    this.showLoader = true;
    this.clearChart();

    this.http.post(this.url + '/createposts/' + this.authorForm.value.authorId, null)
      .subscribe({
        next: (result) => {
          this.showAuthors();
          this.showLoader = false;
          if (result == true) {
            alert('Author data successfully populated!');
          }
          else {
            alert('Invalid Author Id');
          }
          this.authorForm.patchValue({
            author: '',
            chartType: 'pie',
            showLegend: false
          });
        },
        error: (err) => {
          console.error(err);
          this.authorForm.patchValue({
            author: ''
          });
        },
        complete: () => console.info('Populate data completed')
      });
  }

  fillCategory() {
    this.counts = [];
    this.authorForm.patchValue({
      category: ''
    });
    this.totalPosts = 0;
    this.categories = [];
    this.counts = [];
    this.authorForm.patchValue({
      authorId: this.authorForm.value.author.authorId,
    });
    if (!this.authorForm.value.author.authorId) {
      return;
    }
    this.http.get<Categroy[]>(this.url + '/getcategory/' + this.authorForm.value.author.authorId)
      .subscribe({
        next: (result) => {
          result.forEach(x => {
            this.totalPosts += x.count;
            this.categories.push(x.name);
            this.counts.push(x.count);
          });
          if (!result || result.length == 0) return;

          this.chartData = {
            labels: this.categories,
            datasets: [
              {
                data: this.counts,
              }
            ]
          };

          this.chartOptions = {
            responsive: true,
            plugins: {
              title: {
                display: true,
                text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
              },
              legend: {
                display: this.authorForm.value.showLegend
              },
            },
          };
        },
        error: (err) => {
          console.error(err);
        },
        complete: () => { console.info('Fill category completed') }
      });
  }

  changeLegends() {
    this.chartOptions = {
      responsive: true,
      plugins: {
        title: {
          display: true,
          text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
        },
        legend: {
          display: this.authorForm.value.showLegend
        },
      },
    };
  }

  clearChart() {
    this.chartData = {
      labels: [],
      datasets: [
        {
          data: [],
        }
      ]
    };
    this.chartOptions = {
      responsive: true,
      plugins: {
        title: {
          display: true,
          text: '',
        },
        legend: {
          display: false
        },
      },
    };
  }

}

interface Author {
  authorId: string;
  author: string;
  count: number;
}

interface Categroy {
  name: string;
  count: number;
}

We have declared a ChartData and ChartOptions variable inside the component. 

chartData: ChartData<'pie'> = {
    labels: [],
    datasets: [
      {
        data: [],
      }
    ]
  };

  chartOptions: ChartOptions = {
    responsive: true,
    plugins: {
      title: {
        display: true,
        text: '',
      },
      legend: {
        display: false
      },
    },
  };

Labels and data are the important properties of ChartData. 

While loading the component, chart data is empty. Once we fetch the data from backend Web API, ChartData variable will be reassigned with new values inside fillCategory method.  

We are mainly using labels and data properties inside the ChartData.  

this.chartData = {
            labels: this.categories,
            datasets: [
              {
                data: this.counts,
              }
            ]
          };

Chart legends are controlled by legend property inside the ChartOptions.  

 this.chartOptions = {
            responsive: true,
            plugins: {
              title: {
                display: true,
                text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
              },
              legend: {
                display: this.authorForm.value.showLegend
              },
            },
          };

We have also added a populateData method in the above component class. So that user can enter a C# Corner author Id and populate their data from Web API.  

We can change the html template with the code changes below.  

app.component.html 

<app-nav-menu></app-nav-menu>
<form novalidate [formGroup]="authorForm">
  <div class="card row card-row">
    <div class="card-header">
      <div class="row">
        <div class="col-md-6">
          <img src="../assets/c-sharpcorner.png" class="logo"> C# Corner Author Analytics
        </div>
        <div class="col-md-6 total-author-position">
          <label>Total</label>&nbsp;<label class="total-author-color-change">Authors</label>  populated so far : {{authors.length}}
        </div>
      </div>
    </div>

    <div class="card-body">
      <div class="row">
        <div class="col-md-6">
          <div class="form-group row mb-4">
            <label class="col-md-3 col-form-label" for="authorId">Author Id</label>
            <div class="col-md-4">
              <input class="form-control" id="authorId" formControlName="authorId" type="text"
                placeholder="Eg: sarath-lal7" />
            </div>
            <div class="col-md-5">
              <button class="btn btn-primary mr-3" (click)="populateData()">
                Populate Author Data
              </button>
            </div>
          </div>

          <div class="form-group row mb-4">
            <label class="col-md-3 col-form-label" for="authorId">Author Name</label>
            <div class="col-md-4">
              <select class="form-select" formControlName="author" (ngModelChange)="fillCategory()" id="authorId">
                <option value="" disabled>Select an Author</option>
                <option *ngFor="let myauthor of authors" [ngValue]="myauthor">{{myauthor.author}} </option>
              </select>
            </div>
            <label class="col-md-2 col-form-label" for="chartType">Chart Type</label>
            <div class="col-md-3">
              <select id="chartType" class="form-select" formControlName="chartType">
                <option value="pie">Pie</option>
                <option value="doughnut">Doughnut</option>
                <option value="polarArea">Polar Area</option>
                <option value="radar">Radar</option>
                <option value="bar">Bar</option>
                <option value="line">Line</option>
              </select>
            </div>
          </div>

          <div class="form-group row mb-4" *ngIf="categories.length>0">
            <b class="col-md-7"> Total Categories : {{ categories.length}} &nbsp; &nbsp; Total Posts :
              {{totalPosts}}</b>
            <div class="col-md-4">
              <div class="form-check">
                <input class="form-check-input" type="checkbox" formControlName="showLegend"
                  (ngModelChange)="changeLegends()">
                <label class="form-check-label" for="flexCheckChecked">
                  Show Chart Legends
                </label>
              </div>
            </div>
          </div>
        </div>
        <div class="col-md-6">
          <div class="chart-container chart-position" *ngIf="categories.length>0">
            <canvas baseChart [data]="chartData" [type]="authorForm.value.chartType" [options]="chartOptions">
            </canvas>
          </div>
          <div class="file-loader" *ngIf="showLoader">
            <div class="upload-loader">
              <div class="loader"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

We have created a canvas inside the above template file and passed data, chart type and options properties using corresponding variables. 

<canvas baseChart [data]="chartData" [type]="authorForm.value.chartType" [options]="chartOptions">
            </canvas>

We have also added a dropdown for choosing several types of charts.  

 <select id="chartType" class="form-select" formControlName="chartType">
                <option value="pie">Pie</option>
                <option value="doughnut">Doughnut</option>
                <option value="polarArea">Polar Area</option>
                <option value="radar">Radar</option>
                <option value="bar">Bar</option>
                <option value="line">Line</option>
              </select>

Whenever the user chooses the chart type from the above dropdown, it will automatically affect the chart type of the canvas.  

We can change the stylesheet with the code changes below. 

app.component.css 

/* Spin Start*/

.file-loader {
    background-color: rgba(0, 0, 0, .5);
    overflow        : hidden;
    position        : fixed;
    top             : 0;
    left            : 0;
    right           : 0;
    bottom          : 0;
    z-index         : 100000 !important;
  }
  
  .upload-loader {
    position : absolute;
    width    : 60px;
    height   : 60px;
    left     : 50%;
    top      : 50%;
    transform: translate(-50%, -50%);
  }
  
  .upload-loader .loader {
    border           : 5px solid #f3f3f3 !important;
    border-radius    : 50%;
    border-top       : 5px solid #005eb8 !important;
    width            : 100% !important;
    height           : 100% !important;
    -webkit-animation: spin 2s linear infinite;
    animation        : spin 2s linear infinite;
  }
  
  @-webkit-keyframes spin {
    0% {
      -webkit-transform: rotate(0deg);
    }
  
    100% {
      -webkit-transform: rotate(360deg);
    }
  }
  
  @keyframes spin {
    0% {
      transform: rotate(0deg);
    }
  
    100% {
      transform: rotate(360deg);
    }
  }
  
  /* Spin End*/
  
  .card-row {
    margin: 40px;
    height: 600px;
  }
  
  .card-header {
    background-color: azure;
    font-weight     : bold;
  }
  
  .total-author-position {
    text-align: right;
  }
  
  .total-author-color-change {
    color: blue;
  }
  
  .logo {
    width: 30px;
  }
  
  .chart-position {
    position: relative;
    height  : 17vh;
    width   : 34vw
  }

We have added a spinner to the application. The styles for the spinner are added in the above stylesheet file.  

We have completed the Angular code also. We can run both .NET 6.0 and Angular 13 applications now. 

We can enter an Author id and populate data by clicking the button. 

After a few moments, data of given author will be populated. 

We can see the category-wise posts count in the chart. You can choose a chart type from the dropdown and see the data in that format. By default, chart type is selected as “Pie”. 

There are 6 types of charts available in the application. 

Users can choose any desired chart type and see the data in that format. 

Users can choose the show chart legend option and see the chart legend if needed. 

We can enter the correct Id of any author and get the posts data of that author. I have entered the Id of C# Corner founder Mr. Mahesh Chand and populated the data for him.  

Conclusion 

In this post, we have seen how to use Chart.js library along with Ng2-charts directive to create attractive charts in Angular 13. We have used a .NET 6.0 Web API to populate data of C# Corner Author’s articles/blogs using web scraping and dynamically loaded that information in our chart. We have also supplied 6 distinct types of charts (Pie, Doughnut, Polar Area, Radar, Bar, and Line) and users can choose any type of chart from a dropdown and see the chart instantly.