Building real time charts with Angular 5, Google Charts, SignalR Core, .NET Core 2, Entity Framework Core 2 and SqlTable dependency, part 2

 

<< Back to part 1

Let’s take a look on what does the project look like :

 

To make it work this project contains :

  • A DbContext (GaugesContext.cs) for EntityFramework Core
  • A Hub (GaugeHub.cs) for SignalR that broadcast data
  • A Model that contains strongly typed data to send (Gauge.cs)
  • A Repository exposed with Entity Framework and its Interface (GaugeRepository.cs and IGaugeRepository.cs)
  • A Subscription to Gauge sql table with SqlTableDependency and its Interface (GaugeDatabaseSubscription.cs and IDatabaseSubscription)
  • Two Extension methods that extends IServiceCollection (AddDbContextFactory.cs) and IApplicationBuilder (UseSqlTableDependency.cs)
  • And Startup.cs and Program.cs

Let’s describe their implementation :

Gauge Model

using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SignalrCoreDemoWithSqlTableDependency.Models
{
    [Table("GaugesData")]
    public class Gauge
    {
        [JsonProperty("id")]
        [Key]
        public int Id { get; set; }

        [JsonProperty("memory")]
        public int Memory { get; set; }

        [JsonProperty("cpu")]
        public int Cpu { get; set; }

        [JsonProperty("network")]
        public int Network { get; set; }
    }
}

Gauge DbContext

using Microsoft.EntityFrameworkCore;
using SignalrCoreDemoWithSqlTableDependency.Models;

namespace SignalrCoreDemoWithSqlTableDependency.EF
{
    public class GaugeContext : DbContext
    {
        public GaugeContext(DbContextOptions options)
            : base(options)
        {
        }

        public virtual DbSet Gauges { get; set; }
    }
}

Gauge Repository

using SignalrCoreDemoWithSqlTableDependency.EF;
using SignalrCoreDemoWithSqlTableDependency.Models;
using System;
using System.Linq;

namespace SignalrCoreDemoWithSqlTableDependency.Repository
{
    public interface IGaugeRepository
    {
       Gauge Gauge { get; }
    }
    public class GaugeRepository : IGaugeRepository
    {
        private Func _contextFactory;

        public Gauge Gauge => GetGauge();

        public GaugeRepository(Func context)
        {
            _contextFactory = context;
        }

        private Gauge GetGauge()
        {
            using (var context = _contextFactory.Invoke())
            {
                return context.Gauges.FirstOrDefault();
            }
        }
    }
}

Just take a look at how the GaugeContext is injected, I inject a Func that provide an instance of DbContext?

Why? because the lifecycle of our hub must be singleton, so all interdependencies requires between classes have to be singleton too, but we can’t use a DbContext in singleton because the Entity Framework context is not thread safe and in concurrence scenarios the context has a lot of issues.

Gauge Hub

using Microsoft.AspNetCore.SignalR;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using System.Threading.Tasks;

namespace SignalrCoreDemoWithSqlTableDependency.Hubs
{
    public class GaugeHub : Hub
    {
        private readonly IGaugeRepository _repository;

        public GaugeHub(IGaugeRepository repository)
        {
            _repository = repository;
        }

        public async Task GetGaugesData()
        {
            await Clients.All.InvokeAsync("GetGaugesData", _repository.Gauge);
        }
    }
}

The Gauge Hub must inherit from Hub class

“GetGaugesData” is the name of the opretation the client will subscribe to get Gauges data (Clients.All.InvokeAsync(“GetGaugesData”, _repository.Gauge))

GaugeDatabaseSubscription

This is the nost exciting part, this class provides subscription to any modification on Gauge table and notify our app of these modifications, then SignalR will boradcoast it to the client, let’s take a look :

using Microsoft.AspNetCore.SignalR;
using SignalrCoreDemoWithSqlTableDependency.Hubs;
using SignalrCoreDemoWithSqlTableDependency.Models;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using System;
using TableDependency.Enums;
using TableDependency.EventArgs;
using TableDependency.SqlClient;

namespace SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies
{
    public interface IDatabaseSubscription
    {
       void Configure(string connectionString);
    }
    public class GaugeDatabaseSubscription : IDatabaseSubscription
    {
        private bool disposedValue = false;
        private readonly IGaugeRepository _repository;
        private readonly IHubContext _hubContext;
        private SqlTableDependency _tableDependency;

        public GaugeDatabaseSubscription(IGaugeRepository repository, IHubContext hubContext)
        {
            _repository = repository;
            _hubContext = hubContext;
        }

        public void Configure(string connectionString)
        {
            _tableDependency = new SqlTableDependency(connectionString, null, null, null, null, DmlTriggerType.All);
            _tableDependency.OnChanged += Changed;
            _tableDependency.OnError += TableDependency_OnError;
            _tableDependency.Start();

            Console.WriteLine("Waiting for receiving notifications...");
        }

        private void TableDependency_OnError(object sender, TableDependency.EventArgs.ErrorEventArgs e)
        {
            Console.WriteLine($"SqlTableDependency error: {e.Error.Message}");
        }

        private void Changed(object sender, RecordChangedEventArgs e)
        {
            if (e.ChangeType != ChangeType.None)
            {
                // TODO: manage the changed entity
                var changedEntity = e.Entity;
                _hubContext.Clients.All.InvokeAsync("GetGaugesData", _repository.Gauge);
            }
        }

        #region IDisposable

        ~GaugeDatabaseSubscription()
        {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _tableDependency.Stop();
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}

Important things  :

We want to be notified on any changes on the Gaube table, thta’s why I put DmlTriggerType.All on the SqlTableDependency constructor

In Changed event we invoke the broadcast data modification via GaugeHub (_hubContext.Clients.All.InvokeAsync(“GetGaugesData”, _repository.Gauge))

AddDbContextFactory and UseSqlTableDependency

We need these extensions methods to apply in the Startup.cs  our app injection dependency for our GaugeContext and apply as well our SqlTableDependency service

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public static class AddDbContextFactoryHelper
    {
        public static void AddDbContextFactory(this IServiceCollection services, string connectionString) where DataContext : DbContext
        {
            services.AddSingleton<Func>((ctx) =>
            {
                var options = new DbContextOptionsBuilder()
                    .UseSqlServer(connectionString)
                    .Options;

                return () => (DataContext)Activator.CreateInstance(typeof(DataContext), options);
            });
        }
    }
}
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public static class UseSqlTableDependencyHelpers
    {
        public static void UseSqlTableDependency(this IApplicationBuilder services, string connectionString) where T : IDatabaseSubscription
        {
            var serviceProvider = services.ApplicationServices;
            var subscription = serviceProvider.GetService();
            subscription.Configure(connectionString);
        }
    }
}

At last Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using SignalrCoreDemoWithSqlTableDependency.EF;
using SignalrCoreDemoWithSqlTableDependency.Hubs;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public class Startup
    {
        private const string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=SignalRDemo;Integrated Security=SSPI;";
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddSignalR();

            // dependency injection
            services.AddDbContextFactory(ConnectionString);
            services.AddSingleton<IGaugeRepository, GaugeRepository>();
            services.AddSingleton<IDatabaseSubscription, GaugeDatabaseSubscription>();
            services.AddSingleton<IHubContext, HubContext>();

            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader());
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseCors("CorsPolicy");

            app.UseSignalR(routes =>
            {
                routes.MapHub("gauges");
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            app.UseSqlTableDependency(ConnectionString);

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

 

The back end is now completly set up 🙂

Let’s go to see how we setup the front end: part 3

Using Google Charts in Angular 4 project, part 2

Integrate Google Charts and make reusable chart components

We previously saw how to use Google Charts in an classical HTML5 / Javascript page, now it’s time to see how we can make it work in an Angular 4 project. We will still use use the chart as sample (Google Pie Chart)

Let’s get started !

Step 1 :

Add the javascript library file in index.html :

<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

Step 2 :

To make Google Charts more reusable as possible, let’s make a class named GoogleChartsBaseService that does the redundant job : loading the library and manage the callback that we described in the previous article:

declare var google: any;

export class GoogleChartsBaseService {
  constructor() { 
    google.charts.load('current', {'packages':['corechart']});
  }

  protected buildChart(data: any[], chartFunc: any, options: any) : void {
    var func = (chartFunc, options) => {
      var datatable = google.visualization.arrayToDataTable(data);
      chartFunc().draw(datatable, options);
    };   
    var callback = () => func(chartFunc, options);
    google.charts.setOnLoadCallback(callback);
  }
  
}

As you can I added this instruction at the top : declare var google: any;

This is because with TypeScript, and Typescript doesn’t know the definition of google variable, so we declared it, corresponding to the global variable declared in Google Charts library. Typescript will be able to transpile it.

buildChart encapsulate the callback definition, the data mapping (Array to Datatable), and the setOnLoadCallback method call.

Step 3 :

Let’s make a PieChartConfig model to bind it’s configuration :

export class PieChartConfig {
    title: string;
    pieHole: number

    constructor(title: string, pieHole: number) {
        this.title = title;
        this.pieHole = pieHole;
    }
}

title member is the chart title

pieHole is corresponding to the ratio of radii between the hole and the chart (value from 0 to 1)

Step 4 :

Let’s build GooglePieChartService , this service will extends GoogleChartsBaseService 

Because we use Typescript as you know, let’s add at the top : declare var google: any;

We need to encapsulate object google.visualization in a callback (arrow function works too) because this object is not set yet by the framework (during execution of setOnLoadCallback)

Here we are :

import { GoogleChartsBaseService } from './google-charts.base.service';
import { Injectable } from '@angular/core';
import { PieChartConfig } from './../Models/PieChartConfig';

declare var google: any;

@Injectable()
export class GooglePieChartService extends GoogleChartsBaseService {

  constructor() { super(); }

  public BuildPieChart(elementId: String, data: any[], config: PieChartConfig) : void {  
    var chartFunc = () => { return new google.visualization.PieChart(document.getElementById(elementId)); };
    var options = {
            title: config.title,
            pieHole: config.pieHole,
      };

    this.buildChart(data, chartFunc, options);
  }
}

Because it’s an injectable service (singleton), don’t forget to add it in providers array in app.module.ts

We built a base service and PieChartService, that means the logic is set in services and it provides more flexibilty in your app, we decoupled as much as possible Google Charts with our app

Step 5 :

It’s time to make reusable components

we need 3 parameters in input

1- Html id attribute of the div we want to fill with the chart

2- The chart configurations options

3- Data

Here we are:

import { Component, Input, OnInit } from '@angular/core';

import { GooglePieChartService } from './../../Services/google-pie-chart.service';
import { PieChartConfig } from './../../Models/PieChartConfig';

declare var google: any;


@Component({
  selector: 'pie-chart',
  templateUrl: './piechart.component.html'
})
export class PieChartComponent implements OnInit {

    @Input() data: any[];
    @Input() config: PieChartConfig;
    @Input() elementId: String;

    constructor(private _pieChartService: GooglePieChartService) {}

    ngOnInit(): void {
        this._pieChartService.BuildPieChart(this.elementId, this.data, this.config); 
    }
}

the pie chart conponent html file :

<div id="{{elementId}}" style="width: 800px; height: 400px;"></div>

We definitely have to declare them into app.module.ts as well

Step 6 :

Let’s add data, config and try !

We will build a component (named DashboardComponent) that display our reusable ours Pie charts and provide them data.

Ts file :

import { ComboChartConfig } from './../Models/ComboChartConfig';
import { Component } from '@angular/core';
import { PieChartConfig } from './../Models/PieChartConfig';

@Component({
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent {
  title = 'Reusable charts sample';

  data1: any[];
  config1: PieChartConfig;
  elementId1: String;

  data2: any[];
  config2: PieChartConfig;
  elementId2: String;
  
  ngOnInit(): void {     

    //Piechart1 Data & Config
    this.data1 = [['Task', 'Hours per Day'],
    ['Eat',      3],
    ['Commute',  2],
    ['Watch TV', 5],
    ['Video games', 4],
    ['Sleep',    10]];

    this.config1 = new PieChartConfig('My Daily Activities at 20 years old', 0.4);
    this.elementId1 = 'myPieChart1';

    //Piechart2 Data & Config
    this.data2 = [['Task', 'Hours per Day'],
                  ['Work',     11],
                  ['Eat',      2],
                  ['Commute',  2],
                  ['Watch TV', 2],
                  ['Sleep',    7]]

    this.config2 = new PieChartConfig('My Daily Activities at 30 years old', 0.4);
    this.elementId2 = 'myPieChart2';
  }

}

HTML file :

<h2>{{title}}</h2>

<div class="divTable">
    <div class="divTableBody">
        <div class="divTableRow">
            <div class="divTableCell"><pie-chart [data]="data1" [config]="config1" [elementId]="elementId1"></pie-chart></div>
            <div class="divTableCell"><pie-chart [data]="data2" [config]="config2" [elementId]="elementId2"></pie-chart></div>
        </div>
    </div>
</div>

And here is the associated css in dashboard.component.css

Don’t forget to declare that component into app.module.ts

And now …..

Awesome right? 🙂

Now you know how to make reusable Google Charts Angular 4 components! 🙂 🙂 🙂

Here the Url of the project if you want to test in your browser : http://demoangular4.azurewebsites.net/

If you want to get the complete source code you can find it there, you can also contribute! : https://github.com/AnthonyGiretti/Angular4-GoogleCharts

 

 

Create HTML 5 charts export them to PDF for free

sample

During the course of their work, many developers have had to create charts in view of exporting them to a PDF report.
Developing these charts was quite onerous due to having to create an image for display in HTML code, then recreating it on the server side, and finally inserting it into a PDF document created on the fly.
Happily, this is a thing of the past. Thanks to HTML 5, it is now possible to create charts using the new Javascript API without having to create an image. All you need to do is use free library content that will convert an HTML page to PDF by interpreting the HTML, Javascript and CSS.

First, select a graphics library 

I personally chose Google charts, which can be found at https://developers.google.com/chart/.

I like Google charts because of the reliability of Google, it’s free, it offers a plethora of original charts and I’m a fan of Google’s Material Design.
Note that Google uses SVG charts, while other libraries use the canvas. To check browser compatibility, go to the caniuse.com website, which will tell you if the feature you want is compatible with a particular browser. Personally, I test SVG support on the most common browsers.

Following is a 5 step example.

Step 1 : Add a reference to Google Charts

<script type="text/javascript" src="https://www.google.com/jsapi"></script>

Step 2 : Load classes

google.load("visualization", "1", { packages: ["corechart"] });

Step 3 : Implement and define loading for each chart

    google.setOnLoadCallback(drawChatter);
    google.setOnLoadCallback(drawPie);
    google.setOnLoadCallback(drawDonut);
    google.setOnLoadCallback(drawLines);
    google.setOnLoadCallback(drawStacked);

    function drawChatter() {
        var data = google.visualization.arrayToDataTable([
            ['Age', 'Weight'],
            [8, -12],
            [-4, 5.5],
            [11, 14],
            [4, 5],
            [-3, -3.5],
            [6.5, -7]
        ]);

        var options = {
            title: 'Scatter chart',
            hAxis: { title: 'X', minValue: -20, maxValue: 20 },
            vAxis: { title: 'Y', minValue: -20, maxValue: 20 },
            legend: 'none'
        };

        var chart = new google.visualization.ScatterChart(document.getElementById('chartContainer5'));

        chart.draw(data, options);
    }
    
    function drawPie() {

        var data = google.visualization.arrayToDataTable([
            ['Task', 'Hours per Day'],
            ['Work', 11],
            ['Eat', 2],
            ['Commute', 2],
            ['Watch TV', 2],
            ['Sleep', 7]
        ]);

        var options = {
            title: 'Pie chart'
        };

        var chart = new google.visualization.PieChart(document.getElementById('chartContainer4'));

        chart.draw(data, options);
    }

    function drawDonut() {
        var data = google.visualization.arrayToDataTable([
          ['Task', 'Hours per Day'],
          ['Work', 11],
          ['Eat', 2],
          ['Commute', 2],
          ['Watch TV', 2],
          ['Sleep', 7]
        ]);

        var options = {
            title: 'Donut Chart',
            pieHole: 0.4,
        };

        var chart = new google.visualization.PieChart(document.getElementById('chartContainer3'));
        chart.draw(data, options);
    }

    function drawLines() {

        var data = new google.visualization.DataTable();
        data.addColumn('number', 'X');
        data.addColumn('number', 'Y');
        var y = 0;
        var x = 0;
        for (var i = 0; i < 1000; i += 1) {
            y += (Math.random() * 10 - 5);
            x = i - 1000 / 2;
            data.addRows([[x,y]]);
        }
       
        var options = {
            hAxis: {
                title: 'Y'
            },
            vAxis: {
                title: 'X'
            }
        };

        var chart = new google.visualization.LineChart(document.getElementById('chartContainer2'));

        chart.draw(data, options);
    }

    function drawStacked() {
        var data = new google.visualization.DataTable();
        data.addColumn('timeofday', 'X');
        data.addColumn('number', 'Motivation Level');
        data.addColumn('number', 'Energy Level');
        data.addColumn('number', 'Another');

        data.addRows([
          [{ v: [8, 0, 0], f: '8' }, 1, 1.25, 1.25],
        [{ v: [9, 0, 0], f: '9' }, 2, 1.5, 1.75],
        [{ v: [10, 0, 0], f: '10' }, 3, 2, 2.25],
        [{ v: [11, 0, 0], f: '11' }, 4, 3.25, 1.5],
        [{ v: [12, 0, 0], f: '12' }, 5, 3.25, 1.25],
        [{ v: [13, 0, 0], f: '13' }, 6, 4, 1.25],
        [{ v: [14, 0, 0], f: '14' }, 7, 5, 3.25],
        [{ v: [15, 0, 0], f: '15' }, 8, 6.25, 1.75],
        [{ v: [16, 0, 0], f: '16' }, 9, 8.5, 4.25],
        [{ v: [17, 0, 0], f: '17' }, 10, 11, 2.75],
        ]);

        var options = {
            title: 'Stacked Bar',
            isStacked: true,
            hAxis: {
                title: 'Y',
                viewWindow: {
                    min: [7, 30, 0],
                    max: [17, 30, 0]
                }
            },
            vAxis: {
                title: 'x'
            }
        };

        var chart = new google.visualization.ColumnChart(document.getElementById('chartContainer1'));
        chart.draw(data, options);

Step 4 : Create a html placeholder for each chart

<style> 
    .myblock { padding: 5px; text-align: center; vertical-align: middle; } 
</style> 
<body> 
<header><h1>Report</h1></header> 
    <div class="myblock"><div id="chartContainer1" style="height: 300px; width: 50%; margin: auto;"></div></div> 
    <div class="myblock"><div id="chartContainer2" style="height: 300px; width: 50%; margin: auto; "></div></div> 
    <div class="myblock"><div id="chartContainer3" style="height: 300px; width: 50%; margin: auto;"></div></div> 
    <div class="myblock"><div id="chartContainer4" style="height: 300px; width: 50%; margin: auto;"></div></div> 
    <div class="myblock"><div id="chartContainer5" style="height: 300px; width: 50%; margin: auto;" ></div></div> 
</body>

The result is available here : http://chartssample.azurewebsites.net/

Second, download a free NuGet Package here: Select.Pdf

The documentation is available here : http://selectpdf.com/

Server side sample (ASP.NET MVC C#) :

using System;
using System.Web.Mvc;
using SelectPdf;
using PdfPageOrientation = SelectPdf.PdfPageOrientation;
using PdfPageSize = SelectPdf.PdfPageSize;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Report()
        {
            SelectPdf.GlobalProperties.HtmlEngineFullPath = HttpContext.ApplicationInstance.Server.MapPath("~/App_Data/Select.Html.dep");
            try
            {
                string pdf_page_size = "A4";
                PdfPageSize pageSize = (PdfPageSize) Enum.Parse(typeof (PdfPageSize),
                    pdf_page_size, true);

                string pdf_orientation = "Portrait";
                PdfPageOrientation pdfOrientation =
                    (PdfPageOrientation) Enum.Parse(typeof (PdfPageOrientation),
                        pdf_orientation, true);

                int webPageWidth = 1600;

                int webPageHeight = 0;

                // instantiate a html to pdf converter object
                HtmlToPdf converter = new HtmlToPdf();

                // set converter options
                converter.Options.PdfPageSize = pageSize;
                converter.Options.PdfPageOrientation = pdfOrientation;
                converter.Options.WebPageWidth = webPageWidth;
                converter.Options.WebPageHeight = webPageHeight;

                // create a new pdf document converting an url
                PdfDocument doc =
                    converter.ConvertUrl(string.Format("http://{0}:{1}/Home/Index", HttpContext.Request.Url.Host,
                        HttpContext.Request.Url.Port));

                // save pdf document
                doc.Save(HttpContext.ApplicationInstance.Response, false, "Sample.pdf");

                // close pdf document
                doc.Close();

                return Content("");
            }
            catch(Exception e)
            {
                return Content(string.Format("{0}


{1}", e.Message, (null!=e.InnerException)?e.InnerException.Message:"")); } } } }

The pdf file produced is available here: sample.pdf

So what do you think? 🙂