Friday, 23 August 2019

Building a dynamic Gauge in apps using Fiori Elements – Google charts

One of the advantages of Sap Fiori is that you can integrate 3rd party JavaScript APIs with ease. In this blog I will showcase one such use case by building a dynamic Gauge using Google Charts. I will also utilize the Analytic Engine of S/4HANA to generate aggregated data for the gauge with ABAP CDS views (embedded analytics capability).

SAP ABAP Tutorials and Materials, SAP ABAP Study Materials, SAP ABAP Online Exam, SAP ABAP Certifications

I use a simple List report application which displays PM Notifications. There are two Gauges –

1. Shows the notification count
2. Shows the maximum breakdown duration

The “Notification Type” filter on list report along with filtering the data on smart table also filters the gauges data. This means that the gauges are not static but change when filters are applied. Here is the live demo of the application.


First the backend


For aggregating the data dynamically, I built a cube over the Notification CDS (@Analytics.dataCategory: #CUBE). There are two measures Cnt and Breakdown Period and one dimension NotificationType. The Default aggregation ‘Count’ does not work for ABAP CDS views. I created a constant field called ‘Cnt’ in the notification CDS and then used the Default Aggregation ‘SUM’ as a workaround. Similarly, for getting the maximum Breakdown period I used the default aggregation ‘MAX’. The cube aggregates all the entries based on Notification Type(dimension). If notification type is also not requested, then it aggregates all the entries for Notification CDS.

Notification CDS view. Notice the constant field ‘Cnt’ here.

@AbapCatalog.sqlViewName: 'ZCUSTNOTIF'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@OData.publish: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Custom notification'

@ObjectModel.createEnabled: true
@ObjectModel.deleteEnabled: true
@ObjectModel.updateEnabled: true

define view zcustom_notif
  as select from zcustomnotif
{
@UI : {
    lineItem: [{position: 10 }],
    selectionField: [{position: 10}],
    fieldGroup: [{position: 10,
    qualifier: 'Overview1' },{position: 10, qualifier: 'Header1'}]
}
  key qmnum as NotificationNo,
  @UI : {fieldGroup: [{position: 10,
  
    qualifier: 'Overview2' }],
    selectionField: [{position: 20}],
    lineItem: [{position: 20 }]}
  qmtxt as NotificationText,
  @UI : {fieldGroup: [{position: 10,
    qualifier: 'Overview3' }],
    selectionField: [{position: 30}],
    lineItem: [{position: 30 }]}
  qmdat as NotificationDate,
  @UI : {fieldGroup: [{position: 10,
    qualifier: 'Overview4' }],
    selectionField: [{position: 40}],
    lineItem: [{position: 40 }]}
  aufnr as OrderNo,
  @UI : {fieldGroup: [{position: 20,
    qualifier: 'Overview1' }],
    selectionField: [{position: 50}],
    lineItem: [{position: 50 }]}
  qmart as NotificationType, 
   @UI : {fieldGroup: [{position: 20,
    qualifier: 'Overview2' }],
    selectionField: [{position: 60}],
    lineItem: [{position: 60 }]} 
  @Semantics.quantity.unitOfMeasure: 'BreakUnit'
  @EndUserText.label:'Breakdown Duration' 
  eauszt as BreakdownDuration,
  @Semantics.unitOfMeasure: true
  maueh as BreakUnit,  
  
  cast(1 as abap.int4) as Cnt,
  
  objecttyp

}

Cube view


@AbapCatalog.sqlViewName: 'ZCUSTNOTICUBE'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Custom notification Cube'
@Analytics.dataCategory: #CUBE
define view ZCUST_NOTICUBE as select from zcustom_notif {        
    NotificationType, 
    @DefaultAggregation: #MAX
    BreakdownDuration,   
    @DefaultAggregation: #SUM
    Cnt
}

2. A simple query is built over the cube (@Analytics.query: true) and published as OData service (@OData.Publish: true). The service is then activated in SAP frontend server so that it can be consumed by the Fiori application.

@AbapCatalog.sqlViewName: 'ZCUSTNOTIQRY'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Custom notification Query'
@Analytics.query: true
@OData.publish: true
define view ZCUST_NOTIQRY as select from ZCUST_NOTICUBE {        
    NotificationType,    
    BreakdownDuration, 
    Cnt
}

Frontend


1. I extended the List report application to include a new action called ‘Analytical Data’. This action opens a popover to display the Gauges. Manifest Extract –

SAP ABAP Tutorials and Materials, SAP ABAP Study Materials, SAP ABAP Online Exam, SAP ABAP Certifications

2. A simple fragment was built for the popover with two controls which will act as place holders for the gauges and chart. Don’t worry too much about the Combo Chart. It just displays static data. The idea is, if we can use one of the charts, then we integrate any artefact from Google chart API.

Google Charts Uses SVG (Simple Vector Graphics) so we see smooth animations with excellent visualizations. We can use the JavaScript setTimeout() and setInterval() methods to refresh the data and you can see the charts moving in runtime in one fluid motion. I demonstrated this in the video by updating the chart data after 2 sec. This will probably make more sense in an IOT scenario where there is a continuous stream of data readings from sensors.

Google charts have good documentation and easy to use API. 

Fragment –

<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Popover showHeader="false" placement="Bottom">
<content>
<VBox width="400px">
<HBox id="GAU" justifyContent="Center"/>
<HBox id="COM"/>
</VBox>
</content>
</Popover>
</core:FragmentDefinition>

3. I attached a call back function to the search event of the List report. If the user entered ‘Notification Type’ in filter, I use it to filter the Query data as well in OData service call. SADL does rest of the magic.

4. Finally, I used the jQuery.sap.includeScript to load the Google Chart API and customize the chart output.

ListReportExt.controller.js

sap.ui.controller("zcustomnotif.ext.controller.ListReportExt", {
cnt: 0,
breakdn : 0,
onInit: function () {
that = this;
this._loadChart();
smFilt = this.getView().byId(
"zcustomnotif::sap.suite.ui.generic.template.ListReport.view.ListReport::zcustom_notif--listReportFilter"
);
smFilt.attachSearch(this.onSearch);
},

onSearch: function () {
var fildata = smFilt.getFilterData();
if (fildata) {
var filExtract = fildata.NotificationType ? fildata.NotificationType.ranges[0] : null;
if (filExtract) {
var filter = [];
var ftext = new sap.ui.model.Filter(filExtract.keyField, filExtract.operation, filExtract.value1);
filter.push(ftext);
// In case Notification Type filter is used pass it to query
that._loadChart(filter);
} else {
that._loadChart();
}
} else {
that._loadChart();
}
},

_loadChart: function (filter) {
var oModel = this.getOwnerComponent().getModel("Query");
var url = "/ZCUST_NOTIQRY";
var params = {
async: false,
filters: filter,
urlParameters: '$select=Cnt,BreakdownDuration',
success: function (oData, controller) {
sap.ui.core.BusyIndicator.hide();
//Data that gauge will use
that.cnt = oData.results[0].Cnt;
that.breakdn = oData.results[0].BreakdownDuration;
},
error: function (oError) {
sap.ui.core.BusyIndicator.hide();
}
};
oModel.read(url, params);
},

onAnalyticalData: function (oEvent) {
if (!this._oPopover) {
this._oPopover = sap.ui.xmlfragment("zcustomnotif.ext.fragment.Popover", this);
this.getView().addDependent(this._oPopover);
}
//Place holders for Gauge and Combo chart
gauge = this._oPopover.getContent()[0].getItems()[0];
cha = this._oPopover.getContent()[0].getItems()[1];

jQuery.sap.includeScript("https://www.gstatic.com/charts/loader.js", "NewId", function () {
google.charts.load('current', {
'packages': ['gauge']
});
google.charts.setOnLoadCallback(drawChart);

function drawChart() {

var data = google.visualization.arrayToDataTable([
['Label', 'Value'],
['Count', Number(that.cnt)],
['Break HR', Number(that.breakdn)]
]);

var options = {
width: 600,
height: 180,
greenFrom: 0,
greenTo: 7,
redFrom: 9,
redTo: 10,
yellowFrom: 7,
yellowTo: 9,
minorTicks: 5,
max: 10
};
//Notice the use of gauge Place Holder
var chart = new google.visualization.Gauge(document.getElementById(gauge.getId()));
chart.draw(data, options);
//setTimeout function used to incriment the count by 1 
//after 2 sec to demonstrate needle movement
setTimeout(function () {
data.setValue(0, 1, Number(that.cnt) + 1);
chart.draw(data, options);
}, 2000);

}

google.charts.load('current', {
'packages': ['corechart']
});
google.charts.setOnLoadCallback(drawVisualization);

function drawVisualization() {
// Some raw data (not necessarily accurate)
var data = google.visualization.arrayToDataTable([
['Month', 'Bolivia', 'Ecuador', 'Madagascar'],
['2004/05', 165, 938, 522],
['2005/06', 135, 1120, 599],
['2006/07', 157, 1167, 587],
['2007/08', 139, 1110, 615],
['2008/09', 136, 691, 629]
]);

var options = {
title: 'Monthly Coffee Production by Country',
vAxis: {
title: 'Cups'
},
hAxis: {
title: 'Month'
},
seriesType: 'bars',
legend: {
position: 'bottom'
},
series: {
2: {
type: 'line'
}
}
};
//Notice the use of Chart placeholder
var chart = new google.visualization.ComboChart(document.getElementById(cha.getId()));
chart.draw(data, options);
}
});

var analyticButton = this.getView().byId(
"zcustomnotif::sap.suite.ui.generic.template.ListReport.view.ListReport::zcustom_notif--AnalyticalData");
//Attach the popover to the action
this._oPopover.openBy(analyticButton);
}
});

Rest of the source code files –

annotations.xml


<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
<edmx:Reference Uri="/sap/bc/ui5_ui5/ui2/ushell/resources/sap/ushell/components/factsheet/vocabularies/UI.xml">
<edmx:Include Alias="UI" Namespace="com.sap.vocabularies.UI.v1"/>
</edmx:Reference>
<edmx:Reference Uri="/sap/opu/odata/sap/ZCUSTOM_NOTIF_CDS/$metadata">
<edmx:Include Alias="ZCUSTOM_NOTIF_CDS" Namespace="ZCUSTOM_NOTIF_CDS"/>
</edmx:Reference>
<edmx:DataServices>
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="zcustomnotif.ZCUSTOM_NOTIF_CDS">
<!--=============================================================
                Entity Type from chosen collection 
                ===============================================================-->
<Annotations Target="ZCUSTOM_NOTIF_CDS.zcustom_notifType">
<Annotation Term="UI.HeaderInfo">
<Record Type="UI.HeaderInfoType">
<PropertyValue Property="TypeName" String="{@i18n&gt;NOTIFICATION}"/>
<PropertyValue Property="TypeNamePlural" String="{@i18n&gt;NOTIFICATIONS}"/>
<PropertyValue Property="Title">
<Record Type="UI.DataField">
<PropertyValue Property="Value" Path="objecttyp"/>
</Record>
</PropertyValue>
</Record>
</Annotation>
<Annotation Term="UI.HeaderFacets">
<Collection>
<Record Type="UI.ReferenceFacet">
<PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#Header1"/>
</Record>
</Collection>
</Annotation>
<Annotation Term="UI.Facets">
<Collection>
<Record Type="UI.CollectionFacet">
<PropertyValue Property="ID" String="GeneralInformation"/>
<PropertyValue Property="Label" String="{@i18n&gt;OVERVIEW}"/>
<PropertyValue Property="Facets">
<Collection>
<Record Type="UI.ReferenceFacet">
<PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#Overview1"/>
</Record>
<Record Type="UI.ReferenceFacet">
<PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#Overview2"/>
</Record>
<Record Type="UI.ReferenceFacet">
<PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#Overview3"/>
</Record>
<Record Type="UI.ReferenceFacet">
<PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#Overview4"/>
</Record>
</Collection>
</PropertyValue>
</Record>
</Collection>
</Annotation>
</Annotations>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

manifest.json


{
"_version": "1.8.0",
"sap.app": {
"id": "zcustomnotif",
"type": "application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"title": "{{appTitle}}",
"description": "{{appDescription}}",
"tags": {
"keywords": []
},
"dataSources": {
"mainService": {
"uri": "/sap/opu/odata/sap/ZCUSTOM_NOTIF_CDS/",
"type": "OData",
"settings": {
"annotations": [
"ZCUSTOM_NOTIF_CDS_VAN",
"localAnnotations"
],
"localUri": "localService/metadata.xml"
}
},
"ZCUSTOM_NOTIF_CDS_VAN": {
"uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='ZCUSTOM_NOTIF_CDS_VAN',Version='0001')/$value/",
"type": "ODataAnnotation",
"settings": {
"localUri": "localService/ZCUSTOM_NOTIF_CDS_VAN.xml"
}
},
"localAnnotations": {
"uri": "annotations/annotations.xml",
"type": "ODataAnnotation",
"settings": {
"localUri": "annotations/annotations.xml"
}
},
"ZCUST_NOTIQRY_CDS": {
"uri": "/sap/opu/odata/sap/ZCUST_NOTIQRY_CDS/",
"type": "OData",
"settings": {
"localUri": "localService/ZCUST_NOTIQRY_CDS/metadata.xml"
}
}
},
"offline": false,
"sourceTemplate": {
"id": "servicecatalog.connectivityComponentForManifest",
"version": "0.0.0"
},
"crossNavigation": {
"inbounds": {
"intent1": {
"signature": {
"parameters": {},
"additionalParameters": "allowed"
},
"semanticObject": "zcustnot",
"action": "manage"
}
}
}
},
"sap.ui": {
"technology": "UI5",
"icons": {
"icon": "",
"favIcon": "",
"phone": "",
"phone@2": "",
"tablet": "",
"tablet@2": ""
},
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": true
},
"supportedThemes": [
"sap_hcb",
"sap_belize"
]
},
"sap.ui5": {
"resources": {
"js": [],
"css": []
},
"dependencies": {
"minUI5Version": "1.38.34",
"libs": {},
"components": {}
},
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/i18n.properties"
},
"@i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/i18n.properties"
},
"i18n|sap.suite.ui.generic.template.ListReport|zcustom_notif": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/ListReport/zcustom_notif/i18n.properties"
},
"i18n|sap.suite.ui.generic.template.ObjectPage|zcustom_notif": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/ObjectPage/zcustom_notif/i18n.properties"
},
"": {
"dataSource": "mainService",
"preload": true,
"settings": {
"defaultBindingMode": "TwoWay",
"defaultCountMode": "Inline",
"refreshAfterChange": false
}
},
"Query": {
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultBindingMode": "OneWay",
"defaultCountMode": "Request"
},
"dataSource": "ZCUST_NOTIQRY_CDS",
"preload": true
}
},
"extends": {
"extensions": {
"sap.ui.controllerExtensions": {
"sap.suite.ui.generic.template.ListReport.view.ListReport": {
"controllerName": "zcustomnotif.ext.controller.ListReportExt",
"sap.ui.generic.app": {
"zcustom_notif": {
"EntitySet": "zcustom_notif",
"Actions": {
"Actionzcustom_notif1": {
"id": "AnalyticalData",
"text": "{@i18n>Actionzcustom_notif1}",
"press": "onAnalyticalData"
}
}
}
}
}
}
}
},
"contentDensities": {
"compact": true,
"cozy": false
}
},
"sap.ui.generic.app": {
"_version": "1.3.0",
"settings": {
"forceGlobalRefresh": false,
"inboundParameters": {
"qmnum": {
"useForCreate": true
},
"aufnr": {
"useForCreate": true
},
"qmdat": {
"useForCreate": true
}
}
},
"pages": {
"ListReport|zcustom_notif": {
"entitySet": "zcustom_notif",
"component": {
"name": "sap.suite.ui.generic.template.ListReport",
"list": true,
"settings": {
"smartVariantManagement": true
}
},
"pages": {
"ObjectPage|zcustom_notif": {
"entitySet": "zcustom_notif",
"component": {
"name": "sap.suite.ui.generic.template.ObjectPage"
}
}
}
}
}
},
"sap.platform.hcp": {
"uri": ""
}
}

1 comment: