Process Mining

 View Only

Heatmap - a sample plugin widget

By Marco Bonasoro posted Fri July 15, 2022 10:13 AM

  
IBM Process Mining provides users with customisable dashboards; it is possible to assemble a new one, dropping graph widgets from a rich catalog.
Besides, if you have very specific needs for a particular type of graph, it is possible to extend the catalog developing a Custom plugin widget.

Scope

In order to understand the process involved in the creation of a custom widget, we will develop a simple heatmap widget (this work extends on the excellent instructions from “Dashboard - Widgets - Custom plugins” paper, by Patrick Megard).
The scope of this activity was not the development of a professional package, but simply to show how you can demonstrate the flexibility of Process Mining, possibly in a custom demo, or PoC.

Functionalilty

Final user can configure the widget, selecting two attributes (the dimensions of the heatmap) and three values for each attribute. What is shown in a cell is the percentage of cases where the values of the two attributes match the corresponding values from the configuration. As an example, if you have a case with a “City” attribute and and a “Status” attribute, you can produce a graph where the distribution of cases is shown for each combination City/Status among the nine resulting; the percentage is relative to the subset of cases with a valid combination of attributes (so the sum of all cells is always 100%).
The header of the graph show how many cases are considered from the total available.
Cell color coding follows an HSL model, modulating the saturation of the background color, so low percentage are mapped to greyish colours; the tint is selectable from the user (it represents the “Hue” in the model, so 0 is Red, 120 is Green, 240 is Blue and so on).

Configuration

To configure a new heatmap widget, you have to set the following KPI parameters:

KPIAttributeH - the attribute for the horizontal axis
KPIValueH1/2/3 - the values in which we are interested for the H attribute
KPIAttributeV - the attribute for the vertical axis
KPIValueV1/2/3 - the values in which we are interested for the V attribute
KPIHue - the tint to be used for the cell background (see previous paragraph)

Following is a sample widget for a ticketing process, where the two dimensions are the Impact level, and the Country in which request were generated:

sample heatmap

Widget development

Parameters

The Schema section is where KPI parameters are defined.

widget parameters


Backend.js

The code listed below run on the server to preprocess your data.

The init() function initialises the widget, retrieves its parameters, and prepares some service data structures.
Note: grid dimensions are fixed here (3x3); if you modify them, you should also modify accordingly the <table> structure in view.html.

The update() function verifies if there is a match between the values of each case and configured ones; when this happens, the appropriate element of the array filteredCasesHxV is incremented. At the end of execution, the array contains the number of cases matching the conditions.

The finalize() function converts filtered case numbers to percentage, and calculate appropriate value for the Luminance component of the corresponding color. Luminance range is clamped from 90 (0% of matching cases) to 50 (100% of matching cases), in order to associate light colours with low percentages.

(function(){
return {
	init: function(params) {
      // service variables to hold grid dimensions
      this.gridHorDim = 3;
      this.gridVerDim = 3;
      
      // load selected values in arrays
      this.gridHVals = [];
      this.gridHVals.push(params.KPIValueH1);
      this.gridHVals.push(params.KPIValueH2);
      this.gridHVals.push(params.KPIValueH3);

      this.gridVVals = [];
      this.gridVVals.push(params.KPIValueV1);
      this.gridVVals.push(params.KPIValueV2);
      this.gridVVals.push(params.KPIValueV3);
      
      idx = this.gridHorDim + this.gridVerDim * this.gridHorDim;
      this.filteredCasesHxV = []; // array holding results grid (lines1, lines2...)
      for (i = 0; i < idx; i++) {
        this.filteredCasesHxV[i] = 0;
      }
      
	  this.KPIAttributeH = params.KPIAttributeH;
        this.KPIAttributeV = params.KPIAttributeV;
        this.KPIValueH1 = params.KPIValueH1;
        this.KPIValueH2 = params.KPIValueH2;
        this.KPIValueH3 = params.KPIValueH3;
        this.KPIValueV1 = params.KPIValueV1;
        this.KPIValueV2 = params.KPIValueV2;
        this.KPIValueV3 = params.KPIValueV3;
        this.totalCases = 0;
        this.KPIHue = params.KPIHue;
	},

	update: function(trace) {

	if(trace.getDiscarded == 1) {
		return;
	}
      this.totalCases++;
      
      for (var i = 0; i < trace.size(); i++){
        var event = trace.get(i);
        var KPIValueH = event.getStringAttributeValue(this.KPIAttributeH);
        var KPIValueV = event.getStringAttributeValue(this.KPIAttributeV);
        
        for (hi = 0; hi < this.gridHorDim; hi++) {
          for (vi = 0; vi < this.gridVerDim; vi++) {
            if (KPIValueH == this.gridHVals[hi] && KPIValueV == this.gridVVals[vi]){
              idx = hi + vi * this.gridHorDim;
              this.filteredCasesHxV[idx] = this.filteredCasesHxV[idx] + 1;
              return;
            }
          }
        }
        
      }
	},
	
	finalize: function(output) {

        shownCases = 0;
        for (hi = 0; hi < this.gridHorDim; hi++) {
          for (vi = 0; vi < this.gridVerDim; vi++) {
            shownCases += this.filteredCasesHxV[hi + vi * gridHorDim];
          }
        }
        output.shownCases = shownCases;
      
        //output.filteredCasesHxV = this.filteredCasesHxV;
      
        PercentHxV = [];
        LumHxV = [];
        for (hi = 0; hi < this.gridHorDim; hi++) {
          for (vi = 0; vi < this.gridVerDim; vi++) {
            idx = hi + vi * this.gridHorDim;
            PercentHxV[idx] = this.filteredCasesHxV[idx] / shownCases * 100;
            LumHxV[idx] = 90 - PercentHxV[idx] * 0.4;
          }
        }
        output.PercentHxV = PercentHxV;
        output.LumHxV = LumHxV;
 
        output.totalCases = this.totalCases;
        output.KPIHue = this.KPIHue;
      
        output.KPIValueH1 = this.KPIValueH1;
        output.KPIValueH2 = this.KPIValueH2;
        output.KPIValueH3 = this.KPIValueH3;
        output.KPIValueV1 = this.KPIValueV1;
        output.KPIValueV2 = this.KPIValueV2;
        output.KPIValueV3 = this.KPIValueV3;
	  output.KPIAttributeH = this.KPIAttributeH.replace('attr-custom-','');
	  output.KPIAttributeV = this.KPIAttributeV.replace('attr-custom-','');
	}
};})();


Frontend.js


This code simply pass the data from the back-end into the front-end context.

return {
	init: function(context){
		
	},

	update: function(data, context){
      context.scope.data = data;
      
	},

	resize: function(size, context){

	}
};



Style.css


Basic settings for text and table formatting.

p{
	color:#333;
}
h2{
  text-align: center;
}
table {
  border-collapse: collapse;
  width: 100%;
}

td, th {
  border: 1px solid #dddddd;
  text-align: center;
  padding: 8px;
}

tr {
  background-color: #efefef;
}



View.html


This is the code that display the widget. Variables values transmitted from frontend.js context are extracted using {{}} within HTML.
Note: the <table> has a fixed 3x3 dimension, as those values are set in the variables gridHorDim and gridVerDim in the init() function of backend.js. To change heatmap dimensions, variables’ values and the table must be kept in sync.

<div>
  <p style="text-align: center">Total Cases = {{data.totalCases}} - <b>Shown in Map = {{data.shownCases}}</b></p>
  <b style="background-color:HSL({{data.KPIHue}}, 10%, 86%);">Low</b> - 
  <b style="background-color:HSL({{data.KPIHue}}, 100%, 50%);">High</b>
</div>
 
  <table>
  <tr>
    <th style="background-color:white">{{data.KPIAttributeH}}&rarr;<p>&darr;{{data.KPIAttributeV}}</th>
    <th>{{data.KPIValueH1}}</th>
    <th>{{data.KPIValueH2}}</th>
    <th>{{data.KPIValueH3}}</th>
  </tr>
  <tr>
    <th>{{data.KPIValueV1}}</th>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[0]| number}}%, {{data.LumHxV[0]}}%);">{{data.PercentHxV[0]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[1]| number}}%, {{data.LumHxV[1]}}%);">{{data.PercentHxV[1]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[2]| number}}%, {{data.LumHxV[2]}}%);">{{data.PercentHxV[2]| number}} %</td>
  </tr>
  <tr>
    <th>{{data.KPIValueV2}}</th>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[3]| number}}%, {{data.LumHxV[3]}}%);">{{data.PercentHxV[3]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[4]| number}}%, {{data.LumHxV[4]}}%);">{{data.PercentHxV[4]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[5]| number}}%, {{data.LumHxV[5]}}%);">{{data.PercentHxV[5]| number}} %</td>
  </tr>
  <tr>
    <th>{{data.KPIValueV3}}</th>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[6]| number}}%, {{data.LumHxV[6]}}%);">{{data.PercentHxV[6]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[7]| number}}%, {{data.LumHxV[7]}}%);">{{data.PercentHxV[7]| number}} %</td>
    <td style="background-color:HSL({{data.KPIHue}}, {{data.PercentHxV[8]| number}}%, {{data.LumHxV[8]}}%);">{{data.PercentHxV[8]| number}} %</td>
  </tr>
</table>


Thanks for your attention, and have a nice mining.

Marco Bonasoro - Business Automation Technical Specialist - IBM Technology

1 comment
23 views

Permalink

Comments

Fri March 31, 2023 12:39 AM

Very nice, you can contribute this widget into https://github.com/IBM/processmining/ if you like