Circular Progress Bar – Salesforce Lightning Component

Demo and Complete Source code of Circular Progress Bar, Salesforce Lightning Component

Circular Progress Bar - Salesforce Lightning Component

In this blog post we will create reusable Lightning Component to show progress of record using Circular Progress Bar. This component is mostly build using CSS. Javascript is used only for Lightning component support and calling Apex Class. Check video demo here on how to configure and use this component.

Note – There is updated version of this component here.

Circular Progress Bar LEX Component Capabilities

  1. Size – small, medium , large
  2. Theme – blue, orange , green
  3. Legend – Legend to display
  4. Total – Either Number Or API Name of field. Used to derive percentage of Progress Bar
  5. Actual – Either Number Or API Name of field. Used to derive percentage of Progress Bar. If Object contains percentage type of field, then Total can be blank and this field can only contain API name of field of type percentage

CircularProgressController (Apex Class)

 /**
 * Author	:	Jitendra Zaa
 * Desc		:	Controller class for LEX component CircularProgress.
 * 			:	On the basis of field API name passed, it calculates percentage of completion.
 * */
public class CircularProgressController {    
    @AuraEnabled
    public static Integer computePercentage(String sObjectName, String recordId, String totalValueFieldName, String actualValueFieldName){
        Integer retVal = 0 ;
        String query = null;
        
        if(totalValueFieldName != null && totalValueFieldName.trim() != '' &&  actualValueFieldName != null && actualValueFieldName.trim() != '' ){
            query = 'SELECT '+totalValueFieldName+', '+actualValueFieldName+' FROM '+sObjectName+' WHERE Id =: recordId';
        }
        else if (actualValueFieldName != null && actualValueFieldName.trim() != '' ) {
            query = 'SELECT '+actualValueFieldName+' FROM '+sObjectName+' WHERE Id =: recordId';
        }
        
        if(query != null){
            try{
                List<SOBject> lstObj = Database.query(query);
                if(lstObj.size() > 0){
                    Decimal totalVal = 0;
                    Decimal actualVal = 0; 
                    
                    if(totalValueFieldName != null && totalValueFieldName.trim() != ''){ 
                        totalVal = Decimal.valueOf(String.valueOf(lstObj[0].get(totalValueFieldName)));
                    } 
                    actualVal = Decimal.valueOf(String.valueOf(lstObj[0].get(actualValueFieldName)));                     
                    //Means only 1 API Name was supplied and field type is percentage
                    if(totalVal == 0){
                        retVal = Integer.valueOf(actualVal );
                    }else if (actualVal > 0){
                        retVal = Integer.valueOf( ( actualVal / totalVal ) * 100 );  
                    } 
                }
            }catch(Exception e){}
            
        }         
        return retVal;        
    }
}

CircularProgress.cmp

<aura:component implements="flexipage:availableForAllPageTypes, flexipage:availableForRecordHome, force:hasRecordId, force:hasSObjectName" access="global" controller="CircularProgressController">
    
    <aura:attribute name="recordId" type="Id" description="Id of record on which this component is hosted." />
    <aura:attribute name="sObjectName" type="String" description="API name of record on which this component is hosted." />
    
    <aura:attribute name="Legend" type="String" description="Legend to display" />
    
    <aura:attribute name="perText" type="String" default="0%" description="Text to display inside circle. It is auto calculated field and used internally." />
    <aura:attribute name="cirDeg" type="String" default="0" description="Degree of Progress to show. It is auto calculated field and used internally." />
    
    <aura:attribute name="totalProgress" type="String" default="100" description="Total progress. It can be number OR API name of field." />
    <aura:attribute name="actualProgress" type="String" default="50" description="Actual progress. It can be number OR API name of field." />
    
    <aura:attribute name="theme" type="String" default="green" description="Theme of Circular Progress Bar. Possible values are blue, green, orange." />
    <aura:attribute name="size" type="String" default="small" description="Size of Circular Progress Bar. Possible values are small, medium, big." />
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

<div class="clearFloats slds-align--absolute-center">

<div class="{! ( v.cirDeg >
 179 ) ? 'container p50plus '+v.theme+' '+v.size : 'container '+v.theme +' '+v.size }">
            <span>{!v.perText}</span> 

<div class="slice">

<div class="bar" style="{! '-webkit-transform: rotate('+v.cirDeg+'deg); -moz-transform: rotate('+v.cirDeg+'deg); -ms-transform: rotate('+v.cirDeg+'deg); -o-transform: rotate('+v.cirDeg+'deg); transform: rotate('+v.cirDeg+'deg); -ms-transform: rotate('+v.cirDeg+'deg);'}"></div>

<div class="fill"></div>
            </div>
        </div>
         
    </div>


<div class="clearFloats slds-align--absolute-center legend"> 
        {!v.Legend}
    </div>


</aura:component>

CircularProgressController.js

({
	doInit : function(component, event, helper) {
		helper.doInit(component, event, helper) ;
	}
})

CircularProgressHelper.js

({
	doInit : function(component, event, helper)  {
        helper.computeProgress(component, event, helper);
        
	},
    computeProgress : function(component, event, helper)  {
        var totalVal = component.get("v.totalProgress");
        var actualVal = component.get("v.actualProgress"); 
        
        if(totalVal && actualVal && !isNaN(parseInt(totalVal)) && isFinite(totalVal) && !isNaN(parseInt(actualVal)) && isFinite(actualVal)){
           //parameter is number 
            var percVal = parseInt(actualVal) / parseInt(totalVal) ;
            var progressVal = parseInt(  percVal * 360  ) ;
            
            component.set("v.cirDeg" , progressVal );
            component.set("v.perText" , parseInt(percVal * 100)  +'%' ); 
        }else if(actualVal){
            helper.callApexMethod(component, event, helper, totalVal, actualVal);
        }
    },
    callApexMethod : function(component, event, helper, txt_totalVal, txt_actualVal)  {
        
        var action = component.get('c.computePercentage');
        var txt_recordId = component.get("v.recordId");
        var txt_sObjectName = component.get("v.sObjectName");
        
        action.setParams({
            recordId : txt_recordId,
            sObjectName : txt_sObjectName,
            totalValueFieldName : txt_totalVal,
            actualValueFieldName : txt_actualVal
        });
        
        action.setCallback(this, function(a) {
            if (a.getState() === 'SUCCESS') {
                var percVal = a.getReturnValue() ; 
                var progressVal = parseInt(  (percVal/100) * 360  ) ; 
                component.set("v.cirDeg" , progressVal );
                component.set("v.perText" , parseInt(percVal)  +'%' );              
            }  
        });
        $A.enqueueAction(action);  
    }
})

CircularProgress.css

.THIS.legend{
    font-size: 1.25rem;
    color: rgb(22, 50, 92);
}

.THIS .clearFloats:before, .THIS .clearFloats:after 
{
    content: " "; 
    display: table !important;
}

.THIS  .clearFloats:after {clear: both;}
.THIS .clearFloats {*zoom: 1; }

.THIS .container .slice , .THIS .container.p50plus .slice
{
	clip: rect(auto, auto, auto, auto);
}

.THIS .pie, .THIS .container .bar , .THIS .fill, .THIS .container.p50plus .fill{
	 position: absolute;
	  border: 0.08em solid #307bbb;
	  width: 0.84em;
	  height: 0.84em;
	  clip: rect(0em, 0.5em, 1em, 0em);
	  -webkit-border-radius: 50%;
	  -moz-border-radius: 50%;
	  -ms-border-radius: 50%;
	  -o-border-radius: 50%;
	  border-radius: 50%;
	  -webkit-transform: rotate(0deg);
	  -moz-transform: rotate(0deg);
	  -ms-transform: rotate(0deg);
	  -o-transform: rotate(0deg);
	  transform: rotate(0deg);
}

.THIS .pie-fill, .THIS .container.p50plus .bar:after, .THIS .container.p50plus .fill{
	 -webkit-transform: rotate(180deg);
	  -moz-transform: rotate(180deg);
	  -ms-transform: rotate(180deg);
	  -o-transform: rotate(180deg);
	  transform: rotate(180deg);
}

.THIS .container {
	  position: relative;
	  font-size: 120px;
	  width: 1em;
	  height: 1em;
	  -webkit-border-radius: 50%;
	  -moz-border-radius: 50%;
	  -ms-border-radius: 50%;
	  -o-border-radius: 50%;
	  border-radius: 50%;
	  float: left;
	  margin: 0 0.1em 0.1em 0;
	  background-color: #cccccc;
	}

.THIS .container *, .THIS .container *:before, .THIS .container *:after {
	  -webkit-box-sizing: content-box;
	  -moz-box-sizing: content-box;
	  box-sizing: content-box;
	}

.THIS .container.center {
	  float: none;
	  margin: 0 auto;
	}
.THIS .container.big {
	  font-size: 240px;
	}
.THIS .container.small {
	  font-size: 80px;
	}
.THIS .container.medium {
	  font-size: 160px;
	}

.THIS .container > span {
	  position: absolute;
	  width: 100%;
	  z-index: 1;
	  left: 0;
	  top: 0;
	  width: 5em;
	  line-height: 5em;
	  font-size: 0.2em;
	  color: #cccccc;
	  display: block;
	  text-align: center;
	  white-space: nowrap;
	  -webkit-transition-property: all;
	  -moz-transition-property: all;
	  -o-transition-property: all;
	  transition-property: all;
	  -webkit-transition-duration: 0.2s;
	  -moz-transition-duration: 0.2s;
	  -o-transition-duration: 0.2s;
	  transition-duration: 0.2s;
	  -webkit-transition-timing-function: ease-out;
	  -moz-transition-timing-function: ease-out;
	  -o-transition-timing-function: ease-out;
	  transition-timing-function: ease-out;
	}

.THIS .container:after {
	  position: absolute;
	  top: 0.08em;
	  left: 0.08em;
	  display: block;
	  content: " ";
	  -webkit-border-radius: 50%;
	  -moz-border-radius: 50%;
	  -ms-border-radius: 50%;
	  -o-border-radius: 50%;
	  border-radius: 50%;
	  background-color: whitesmoke;
	  width: 0.84em;
	  height: 0.84em;
	  -webkit-transition-property: all;
	  -moz-transition-property: all;
	  -o-transition-property: all;
	  transition-property: all;
	  -webkit-transition-duration: 0.2s;
	  -moz-transition-duration: 0.2s;
	  -o-transition-duration: 0.2s;
	  transition-duration: 0.2s;
	  -webkit-transition-timing-function: ease-in;
	  -moz-transition-timing-function: ease-in;
	  -o-transition-timing-function: ease-in;
	  transition-timing-function: ease-in;
	}

.THIS .container .slice {
	  position: absolute;
	  width: 1em;
	  height: 1em;
	  clip: rect(0em, 1em, 1em, 0.5em);
	}

.THIS .container:hover {
	  cursor: default;
	}

.THIS .container:hover > span {
	  width: 3.33em;
	  line-height: 3.33em;
	  font-size: 0.3em; 
	}

.THIS .container:hover:after {
	  top: 0.04em;
	  left: 0.04em;
	  width: 0.92em;
	  height: 0.92em;
	}

.THIS .container.blue .bar, .THIS .container.blue .fill {
  border-color: #307bbb !important;
}

.THIS .container.blue:hover > span {
  color: #307bbb;
}

.THIS .container.green .bar, .THIS .container.green .fill {
  border-color: #4db53c !important;
}

.THIS .container.green:hover > span {
  color: #4db53c;
}
 
.THIS .container.orange .bar, .THIS .container.orange .fill {
  border-color: #dd9d22 !important;
}

.THIS .container.orange:hover > span {
  color: #dd9d22;
}


CircularProgress.design

<design:component>    
	<design:attribute name="theme" label="Theme" datasource="blue, green , orange" description="Color theme of the component" />
    
    <design:attribute name="size" label="Size" datasource="small, medium, big" description="Size of the of the component" />
    
    <design:attribute name="totalProgress" label="Total" description="Either API Name of field Or numeric value showing total value to be used for computation" />
    <design:attribute name="actualProgress" label="Actual" description="Either API Name of field Or numeric value showing actual value to be used for computation" />
    
    <design:attribute name="Legend" label="Legend" description="Legend to be displayed." />
    
</design:component>

CircularProgress.svg
Download this icon and open in notepad. Copy complete content in this file.

Related posts