Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Screenshot of a live COVID-19 patient count dashboard built with a Salesforce Lightning Aura Component
Lightning

COVID-19 Live Dashboard using Lightning Aura Component

A lightning component that shows the COVID-19 live patient count, with the API callout made without an Apex controller.

The short answer

This post builds a COVID-19 tracking dashboard with a Salesforce Lightning Aura Component and Chart.js. The REST callouts run client side through the native JavaScript Fetch API, so no Apex controller is involved.

Key takeaways Lightning components do not take CDN links, so upload Chart.js as a static resource and pull it in with the ltng:require tag. Call a public REST API straight from a helper method with the native JavaScript fetch method, with no Apex class in the path. Wrap each fetch in a promise and hand them to Promise.all so every callout finishes before you parse the responses and set component attributes. Draw the charts by binding HTML canvas elements to Chart.js doughnut and bar configurations.

Recently, while going through the COVID-19 Live Count link shared on a group, I found Covid19India.org had exposed the API endpoints publicly. Thought to build a dashboard using Charts.js and publish it for guest users using community.

The interesting part with this project is that not a single apex class is involved in making the API callout. All the callouts use the native "Fetch" method from JavaScript. Of course it's not secure for the authenticated callout, but you should give a try.

COVID-19 Live Dashboards using Lightning Aura Component

COVID-19 Live Dashboard

GIT repository is shared at the end of the article. Now, lets go through the components involved:

Components Involved:

Corona_Chart_Scripts.resource

As lightning components do not support CDN links, I downloaded Charts.js from the official website, added it into the zip and uploaded it as a static resource.

India_Corona_Map.cmp

<aura:component implements="flexipage:availableForAllPageTypes,forceCommunity:availableForAllPageTypes" >
    <aura:attribute name="selectedState" type="String" default="Maharashtra"></aura:attribute>
    <aura:attribute name="states" type="list"></aura:attribute>
    <aura:attribute name="stateData" type="Object"></aura:attribute>
    <aura:attribute name="stateChartInstance" type="Object"></aura:attribute>
    <aura:attribute name="isLoading" type="boolean" default="true"></aura:attribute>
    <ltng:require scripts="{!$Resource.Corona_Chart_Scripts+'/Chart.min.js'}" 
                  afterScriptsLoaded="{!c.initialiseSetup}">
    </ltng:require>
    <div class="body">
        <aura:if isTrue="{!!v.isLoading}">
            <div class="slds-grid slds-wrap">
                <div class="slds-col slds-size_1-of-1 slds-large-size_6-of-12">
                    <div class="slds-box slds-m-around_xx-small">
                        <canvas aura:id="dailyCount"></canvas>
                    </div>
                </div>
                <div class="slds-col slds-size_1-of-1 slds-large-size_6-of-12">
                    <div class="slds-box slds-m-around_xx-small">
                        <canvas aura:id="overallCount"></canvas>
                    </div>
                </div>
                <div class="slds-col slds-size_1-of-1 slds-large-size_6-of-12">
                    <div class="slds-box slds-m-around_xx-small">
                        <canvas aura:id="historicalPerDayCount"></canvas>
                    </div>
                </div>
                <div class="slds-col slds-size_1-of-1 slds-large-size_6-of-12">
                    <div class="slds-box slds-m-around_xx-small">
                        <canvas aura:id="historicalTotalCount"></canvas>
                    </div>
                </div>
                <div class="slds-col slds-size_1-of-1">
                    <div class="slds-box slds-m-around_xx-small">
                        <lightning:select label="Select State" value="{!v.selectedState}" onchange="{!c.onSelectState}">
                            <aura:iteration items="{!v.states}" var="state">
                                <option text="{!state}" value="{!state}" selected="{!state == v.selectedState}"></option>
                            </aura:iteration>
                        </lightning:select> 
                        <canvas aura:id="stateCount"></canvas>
                    </div>
                </div>
            </div>
            <aura:set attribute="else">
                <div class="spinnerHolder">
                    <lightning:spinner class="spins" alternativeText="Fetching data from API.." size="large" />
                </div>
            </aura:set>                
        </aura:if>
    </div>
</aura:component>

India_Corona_MapController.js

  • There are two callouts made, so used Promise.all to finish both of them before proceeding with further data operations.
({
    initialiseSetup : function(component, event, helper) {
        Promise.all([helper.fetchData(component, 'https://api.covid19india.org/v2/state_district_wise.json' ,'v.nationalData'),
        helper.fetchData(component, 'https://api.covid19india.org/data.json' ,'v.stateData')])
        .then(values => {
            component.set("v.isLoading", false);
            let nationalData = values[1];
            let stateData = values[0];            
            const lastArrayIndex = nationalData.cases_time_series.length - 1;
            
            //draw donut for todays cases
            helper.todaysCasesDonut(component, nationalData.cases_time_series[lastArrayIndex]);
            
            //draw donut for todays cases
            helper.overallCasesDonut(component, nationalData.cases_time_series[lastArrayIndex]);
            
            //last 5 days count
            const lastFiveDaysData = [];
            for(let i = 0 ; i < 5 ; i ++)
            	lastFiveDaysData.push(nationalData.cases_time_series[lastArrayIndex - i]);
            
            //historical per day count for past 5 days
            helper.historicalPerDayCount(component, lastFiveDaysData);
            
            //historical overall count for past 5 days
            helper.historicalTotalCount(component, lastFiveDaysData);
            
            //find state list
            let states = [];
            let districtDataByState = {};        
            stateData.forEach(st => {
            	states = [...states, st.state];
                districtDataByState[st.state] = st.districtData;
        	});
           
            component.set("v.states", states);
            component.set("v.stateData", districtDataByState);
        
            //statewise count
            helper.stateWiseCount(component);
        })
        .catch( err => console.log(err));
	},
    onSelectState : function(component, event, helper){
        //statewise count
        helper.stateWiseCount(component);
    }
})

India_Corona_MapHelper.js

  • fetchData method is responsible to call the endpoint and pass the response back to the controller.
  • Other methods prepare the required configs for the respective chart types. You can refer to the Chart.Js documentation for any configs.
({
    fetchData : function(component, endpoint, variable){
        return new Promise((resolve, reject) => {
            fetch(endpoint)
            .then(response => {
                return response.json();
            })
            .then(data => {
                resolve(data);
            })
            .catch(err => {
            	reject(err);
            });
        });
    },
    todaysCasesDonut : function (component, todaysCases){
        const chartData = {
            labels: ['Confirmed', 'Deceased', 'Recovered'],
            datasets: [{
                          label: "Daily Cases",
                          backgroundColor: ["#8B0000", "#FF4500","#006400"],
                          data: [todaysCases.dailyconfirmed, todaysCases.dailydeceased, todaysCases.dailyrecovered]
                        }]
        };
        const chartOptions = {
            title: {
                display: true,
                text: 'Todays cases in India'
            },
            animation: {
                duration: 2000
            }
        }
        this.generateChart(component, 'dailyCount', chartData, 'doughnut', chartOptions);
    },
    overallCasesDonut : function (component, overallCases){
        const chartData = {
            labels: ['Confirmed', 'Deceased', 'Recovered'],
            datasets: [{
                          label: "Till Date Cases",
                          backgroundColor: ["#8B0000", "#FF4500","#006400"],
                          data: [overallCases.totalconfirmed, overallCases.totaldeceased, overallCases.totalrecovered]
                        }]
        };
        const chartOptions = {
            title: {
                display: true,
                text: 'Till date cases in India'
            },
            animation: {
                duration: 2000
            }
        }
        this.generateChart(component, 'overallCount', chartData, 'doughnut', chartOptions);
    },
    historicalPerDayCount : function (component, lastFiveDaysData){
        let labels = [];
        let confirmedData = [];
        let deseasedData = [];
        let recoveredData = [];
        lastFiveDaysData.forEach(info => {
            labels = [...labels, info.date];
            confirmedData = [...confirmedData, info.dailyconfirmed];
            deseasedData = [...deseasedData, info.dailydeceased];
            recoveredData = [...recoveredData, info.dailyrecovered];
        });
            
        const chartData = {
            labels,
            datasets: [{
                    label: "Confirmed",
                    backgroundColor: "#8B0000",
                    data:confirmedData
                },{
                    label: "Deceased",
                    backgroundColor: "#FF4500",
                    data: deseasedData
                },{
                    label: "Recovered",
                    backgroundColor: "#006400",
                    data: recoveredData
                }]
            };
        const chartOptions = {
            title: {
                display: true,
                text: 'Per day cases for last 5 days'
            },
            animation: {
                duration: 2000
            }
        }
        this.generateChart(component, 'historicalPerDayCount', chartData, 'bar', chartOptions);
    },
    stateWiseCount : function (component, lastFiveDaysData){
        let labels = [];
        let data = [];
        let backgroundColor = [];
        const selectedState = component.get("v.selectedState");
        const districtInfo = component.get("v.stateData");
        const selectedStateDistrictInfo = districtInfo[selectedState];
        selectedStateDistrictInfo.forEach(dist => {
            labels = [...labels, dist.district];
            data = [...data, dist.confirmed];
            backgroundColor = [...backgroundColor, '#'+(Math.random()*0xFFFFFF<<0).toString(16)];
        })
            
        const chartData = {
            labels,
            datasets: [{
                label: "Districts",
                backgroundColor,
                data
            }]
        }
        const chartOptions = {
            title: {
                display: true,
                text: "Districts level confirmed cases for "+selectedState+" state"
            },
            animation: {
                duration: 2000
            }
        }
        this.generateChart(component, 'stateCount', chartData, 'bar', chartOptions);
    },
    historicalTotalCount : function (component, lastFiveDaysData){
        let labels = [];
        let confirmedData = [];
        let deseasedData = [];
        let recoveredData = [];
        lastFiveDaysData.forEach(info => {
            labels = [...labels, info.date];
            confirmedData = [...confirmedData, info.totalconfirmed];
            deseasedData = [...deseasedData, info.totaldeceased];
            recoveredData = [...recoveredData, info.totalrecovered];
        });
            
        const chartData = {
            labels,
            datasets: [{
                    label: "Confirmed",
                    backgroundColor: "#8B0000",
                    data:confirmedData
                },{
                    label: "Deceased",
                    backgroundColor: "#FF4500",
                    data: deseasedData
                },{
                    label: "Recovered",
                    backgroundColor: "#006400",
                    data: recoveredData
                }]
            };
        const chartOptions = {
            title: {
                display: true,
                text: 'Overall cases for last 5 days'
            },
            animation: {
                duration: 2000
            }
        }
        this.generateChart(component, 'historicalTotalCount', chartData, 'bar', chartOptions);
    },
	generateChart : function(component, chartId, chartData, chartType, chartOptions) {
        var ctx = component.find(chartId).getElement();
        ctx.height = 250;
        if(chartId == 'stateCount' && component.get("v.stateChartInstance") != null)
            component.get("v.stateChartInstance").destroy();
        var lineChart = new Chart(ctx ,{
            type: chartType,
            data: chartData,
            options: chartOptions
        });
        if(chartId == 'stateCount')
            component.set("v.stateChartInstance", lineChart);
	}
})

Corona_Map.app

It's a container which will show the output of the component.

<aura:application extends="force:slds">
	<c:India_Corona_Map></c:India_Corona_Map>
</aura:application>

That is us done from the coding side, so lets review the app. Wait, just getting a spinner instead of a dashboard! Why is this happening? Lets check the browser console.

CSP Trusted Site Error

CSP Trusted Site Error

Yes, the content security policy is blocking the callout. In short, Salesforce does not allow a callout to an endpoint unless it is marked as a CSP Trusted Site.

How to white list the endpoint domain?

  • Navigate to Setup -> Security Controls -> CSP Trusted Sites
  • Create the entry as follows:

CSP Trusted Site Entry

CSP Trusted Site Entry

Voila, It should be working now! Comment if you face any issues.

GIT Repo: https://github.com/SFDCDevs/COVID-19-Live-Dashboards-Salesforce.git

Frequently asked questions

Can you make API callouts from a Lightning Aura Component without Apex?

Yes. The JavaScript controller or helper can make HTTP callouts with the native Fetch API. It only suits public APIs that need no server-side authentication.

How do you load Chart.js in a Lightning Aura Component?

Lightning components do not support external CDN links, so download the Chart.js library, upload it as a static resource in Salesforce, and load it in the component with the ltng:require tag and its afterScriptsLoaded handler.

How do you handle multiple API callouts simultaneously in a Lightning Component?

Wrap each fetch request in a helper method that returns a promise, then pass those promises to Promise.all in the client-side controller so every response is in before you process and render.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment