Plotly charts using Angular, ASP.NET Core 1.0 and Elasticsearch

This article shows how to use a javascript Plotly Bar Chart in Angular to display data from an ASP.NET Core 1.0 MVC application. The server uses Elasticsearch as persistence and uses it to retrieve the aggregated data and return it to the UI.

Code: https://github.com/damienbod/AngularPlotlyAspNetCore

2016.07.05: Updated to ASP.NET Core RTM and ElasticsearchCrud 2.3.3.1
2016.05.20: Updated to ASP.NET Core RC2

Setting up the test data in Elasticsearch

The SnakeBites class is used as the model for the data. This class is used to create a mapping in Elasticsearch and also retrieve the data as required. The GeographicalRegion and the Country properties have ElasticsearchString attributes which are used to define the non default mapping in Elasticsearch. The fields are defined as not analyzed so that a terms aggregation or search will work for the whole text with whitespaces. If these are analyzed, Elasticsearch will split the words, which is not what we what in this use case. EleaticsearchCRUD is used to access the Elasticsearch API, NEST could also be used now as the RC1 version runs in dnxcore.

namespace AngularPlotlyAspNetCore.Models
{
    using System;
    using ElasticsearchCRUD.ContextAddDeleteUpdate.CoreTypeAttributes;
    public class SnakeBites
    {
        [ElasticsearchString(Index = StringIndex.not_analyzed)]
        public string GeographicalRegion { get; set; }

        [ElasticsearchString(Index = StringIndex.not_analyzed)]
        public string Country { get; set; }

        public double NumberOfCasesLow { get; set; }
        public double NumberOfCasesHigh { get; set; }
        public double NumberOfDeathsLow { get; set; }
        public double NumberOfDeathsHigh { get; set; }

    }
}

The index mapping is created as follows:

elasticsearchContext.IndexCreate<SnakeBites>();

When the index and type mapping are created, it can be checked in Elasticsearch with the URL
http://localhost:9200/_mapping

http://localhost:9200 is the default Elasticsearch URL.

The mapping is created with non analyzed fields.
plotlyAngularAspNetCoreElasticsearch_01

Data can then be added using the JSON data in the src folder. This needs to be changed in the configuration file if running locally.

List<SnakeBites> data = JsonConvert.DeserializeObject<List<SnakeBites>>(
	File.ReadAllText(_optionsApplicationConfiguration.Value.FilePath)));
long counter = 1;
foreach (var snakeCountry in data)
{
	// create some documents
	counter++;
	elasticsearchContext.AddUpdateDocument(snakeCountry, counter);
}

elasticsearchContext.SaveChanges();

An AddAllData method has been added to the SnakeDataController class so that is is easily to setup to data. This is just wired up using the default DI in the ASP.NET Core 1.0 application.

Creating a Bar Chart using angular-plotly

A plotly chart is added to the angular template using the angular directive plotly.

<div>
    <plotly data="data" layout="layout" options="options"></plotly>
</div>

This directive needs to be added to the main module before you can use this. This is documented on the github angular-plotly (link above).

var mainApp = angular.module("mainApp",
        [
            "ui.router", 
            "plotly"
        ]);

The plotly directive has three parameters. These can be filled with data in an angular controller. The barChartData (data for the bar chart) which is returned from the API in a HTTP GET, is added to the controller using constructor injection. The chart properties are filled with data and the type property is used to specify the chart type. The demo uses the type ‘bar’ for a bar chart. Each chart requires the data in a different format. This is documented on the plotly web page.

(function () {
	'use strict';

	var module = angular.module('mainApp');

	module.controller('RegionBarChartController',
		[
			'$scope',
			'$log',
            'barChartData',
			RegionBarChartController
		]
	);

	function RegionBarChartController($scope, $log, barChartData) {
	    $log.info("RegionBarChartController called");
	    $scope.message = "RegionBarChartController";

	    $scope.barChartData = barChartData;
	    

	    $scope.RegionName = $scope.barChartData.RegionName;

	    $scope.layout = {
	        title: $scope.barChartData.RegionName + ": Number of snake bite deaths" ,
	        height: 500,
	        width: 1200
	    };

	   
	    function getYDatafromDatPoint() {
	        return $scope.barChartData.NumberOfDeathsHighData.Y;
	    }

	    $scope.data = [
          {
              x: $scope.barChartData.X,
              y: getYDatafromDatPoint(),
              name: $scope.barChartData.Datapoint,
              type: 'bar',
              orientation :'v'
          }
	    ];

	    $log.info($scope.data);
	}
})();

The resource data from the API is added to the controller using the resolve from the angular ui-router module. For example the state overview returns a geographicalRegions object from the server API and the object is injected into the constructor of the OverviewController controller.

mainApp.config(["$stateProvider", "$urlRouterProvider",
		function ($stateProvider, $urlRouterProvider) {
            	$urlRouterProvider.otherwise("/overview");

            	$stateProvider
                    .state("overview", {
                        url: "/overview",
                        templateUrl: "/templates/overview.html",
                        controller: "OverviewController",
                        resolve: {

                            SnakeDataService: "SnakeDataService",

                            geographicalRegions: ["SnakeDataService", function (SnakeDataService) {
                                return SnakeDataService.getGeographicalRegions();
                            }]
                        }

                    }).state("regionbarchart", {
                        url: "/regionbarchart/:region",
		                templateUrl: "/templates/regoinbarchart.html",
		                controller: "RegionBarChartController",
		                resolve: {

		                    SnakeDataService: "SnakeDataService",

		                    barChartData: ["SnakeDataService", "$stateParams", function (SnakeDataService, $stateParams) {
		                        return SnakeDataService.getRegionBarChartData($stateParams.region);
		                }]
		        }
		    });           
		}
	]
    );

The SnakeDataService is used to call the server API using the $http service. The different functions use a promise to return the data.

(function () {
	'use strict';

	function SnakeDataService($http, $log) {

	    $log.info("SnakeDataService called");

	    var getGeographicalRegions = function () {
	        $log.info("SnakeDataService GetGeographicalRegions called");
	        return $http.get("/api/SnakeData/GeographicalRegions")
			.then(function (response) {
				return response.data;
			});
		}

		var getRegionBarChartData = function (region) {
		    $log.info("SnakeDataService getRegionBarChartData: " + region);
		    $log.info(region);
		    return $http.get("/api/SnakeData/RegionBarChart/" + region )
			.then(function (response) {
			    return response.data;
			});
		}

		return {
		    getGeographicalRegions: getGeographicalRegions,
		    getRegionBarChartData: getRegionBarChartData
		}
	}

	var module = angular.module('mainApp');

	// this code can be used with uglify
	module.factory("SnakeDataService",
		[
			"$http",
			"$log",
			SnakeDataService
		]
	);

})();

Setting up the ASP.NET Core server to return the data

The SnakeDataController class is the MVC controller class used for the API. This class uses the ISnakeDataRepository to access the data provider.

using System.Collections.Generic;
using AngularPlotlyAspNetCore.Models;
using Microsoft.AspNetCore.Mvc;

namespace AngularPlotlyAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class SnakeDataController : Controller
    {
        private ISnakeDataRepository _snakeDataRepository;

        public SnakeDataController(ISnakeDataRepository snakeDataRepository)
        {
            _snakeDataRepository = snakeDataRepository;
        }

        [HttpGet("GeographicalRegions")]
        public List<GeographicalRegion> GetGeographicalRegions()
        {
            return _snakeDataRepository.GetGeographicalRegions();
        }

        [HttpGet("RegionBarChart/{region}")]
        public GeographicalCountries GetBarChartDataForRegion(string region)
        {
            return _snakeDataRepository.GetBarChartDataForRegion(region);
        }

        [HttpGet("AddAllData")]
        public IActionResult AddAllData()
        {
            _snakeDataRepository.AddAllData();
            return Ok();
        }
    }
}

The SnakeDataRepository class implements the ISnakeDataRepository interface. This is then defined in the Startup class using the ASP.NET Core 1.0 default IoC.

public void ConfigureServices(IServiceCollection services)
{
	// Add framework services.
	services.AddMvc();

	services.AddScoped<ISnakeDataRepository, SnakeDataRepository>();
}

The SnakeDataRepository implements the Elasticsearch API. The GetGeographicalRegions implements a terms bucket aggregation per region and then a sum metric aggregation on the required properties. The result is then returned in a list of GeographicalRegion objects.

public List<GeographicalRegion> GetGeographicalRegions()
{
	List<GeographicalRegion> geographicalRegions = new List<GeographicalRegion>();
	var search = new Search
	{
		Aggs = new List<IAggs>
		{
			new TermsBucketAggregation("getgeographicalregions", "geographicalregion")
			{
				Aggs = new List<IAggs>
				{
					new SumMetricAggregation("countCases", "numberofcaseshigh"),
					new SumMetricAggregation("countDeaths", "numberofdeathshigh")
				}
			}
		}
	};

	using (var context = new ElasticsearchContext(_connectionString, _elasticsearchMappingResolver))
	{
		var items = context.Search<SnakeBites>(
			search,
			new SearchUrlParameters
			{
				SeachType = SeachType.count
			});

		try
		{
			var aggResult = items.PayloadResult.Aggregations.GetComplexValue<TermsBucketAggregationsResult>("getgeographicalregions");

			foreach (var bucket in aggResult.Buckets)
			{
				var cases = Math.Round(bucket.GetSingleMetricSubAggregationValue<double>("countCases"), 2);
				var deaths = Math.Round(bucket.GetSingleMetricSubAggregationValue<double>("countDeaths"), 2);
				geographicalRegions.Add(
					new GeographicalRegion {
						Countries = bucket.DocCount,
						Name = bucket.Key.ToString(),
						NumberOfCasesHigh = cases,
						NumberOfDeathsHigh = deaths,
						DangerHigh =  (deaths > 1000)
					});


			}
		}
		catch (Exception ex)
		{
		  Console.WriteLine(ex.Message);
		}
	}
		   
	return geographicalRegions;
} 

The GetBarChartDataForRegion method just searches for the data using a simple match query on the geographicalregion field. The size is increased to a 100, as the default Hits from Elasticsearch is set to 10. The result is returned as a GeographicalCountries object which is used for the bar charts.

public GeographicalCountries GetBarChartDataForRegion(string region)
{
	GeographicalCountries result = new GeographicalCountries { RegionName = region};

	var search = new Search
	{
		Query = new Query(new MatchQuery("geographicalregion", region)),
		Size= 100
	};

	using (var context = new ElasticsearchContext(_connectionString, _elasticsearchMappingResolver))
	{
		var items = context.Search<SnakeBites>(search);
		
		result.NumberOfCasesHighData = new BarTrace { Y = new List<double>()};
		result.NumberOfCasesLowData = new BarTrace {Y = new List<double>() };
		result.NumberOfDeathsHighData = new BarTrace {  Y = new List<double>() };
		result.NumberOfDeathsLowData = new BarTrace {  Y = new List<double>() };
		result.X = new List<string>();

		foreach (var item in items.PayloadResult.Hits.HitsResult)
		{
			result.NumberOfCasesHighData.Y.Add(item.Source.NumberOfCasesHigh);
			result.NumberOfCasesLowData.Y.Add(item.Source.NumberOfCasesLow);
			result.NumberOfDeathsHighData.Y.Add(item.Source.NumberOfDeathsHigh);
			result.NumberOfDeathsLowData.Y.Add(item.Source.NumberOfDeathsLow);

			result.X.Add(item.Source.Country);
		}
	}

	return result;
}

Running

When the application is run, the data is displayed in a pro region aggregated form.
plotlyAngularAspNetCoreElasticsearch_02

When the region is clicked, the Plotly Bar chart is displayed showing the difference pro country in that region.
plotlyAngularAspNetCoreElasticsearch_03

Notes

The demo data is from the website: International Society on Toxinology – Global Snakebite Initiative.

Before running the test code, Elasticsearch and ASP.NET Core 1.0 RC1 need to be installed.

Links:

https://plot.ly/javascript/

https://github.com/alonho/angular-plotly

https://www.elastic.co/products/elasticsearch

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: