function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
SticklebackStickleback 

How to destroy chart.js chart in lightning component

Hi all,

We've used the open source chart.js to implement our charts in lightning components but we've noticed that if the chart's data set changes, e.g. a picklist that the user can select a different value from which generates a different data set for the chart, the chart flickers between the old & new values when the mouse moves over it.
Looking around the web the solution from Chart.js is to keep a global variable in the javascript which holds the chart object when it is created & then destroy that chart object before re-creating a new one (see http://stackoverflow.com/questions/28609932/chartjs-resizing-very-quickly-flickering-on-mouseover). However I don't think that mechanism is possible from within a lightning component, but I may be wrong. Any suggestions?

Here's a working example of the problem. After the chart is displayed if you check the checkbox, new data will appear in the chart.  If you then move the mouse over the chart it will toggle between the old & new chart at certain positions. Obviously in real component I'd be calling an apex controller etc. but I thought a simplified version would be easier to understand.  For it to work you'll need to create a static resource called Chart that contains the Chart.js from http://www.chartjs.org/

(AndeeChart.cmp)
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
<ltng:require scripts="{!$Resource.Chart}" afterScriptsLoaded="{!c.init}"/>

<aura:attribute name="dataset" type="String" default="1"  description="Which set of data to display in the chart.  Will be either 1 or 2"/>

<div class="slds-grid slds-wrap">
    <div class="slds-col slds-size--1-of-1 slds-small-size--1-of-2 slds-medium-size--1-of-4">
        <ui:inputCheckbox label="Toggle Data?" click="{!c.updateDataset}"/>

    </div>
    <div class="slds-col slds-size--1-of-1 slds-small-size--1-of-2 slds-medium-size--3-of-4">
        Chart1<br></br>
        <canvas aura:id="andeeChart" id="andeeChart123"/>
    </div>

</div>
(AndeeChartController.js)
({
    init : function(component, event, helper) {
        helper.setupChart(component);
    },
    updateDataset : function(component, event, helper) {
        var dataset = component.get('v.dataset');
        if (dataset == '1'){
            dataset = '2';
        } else {
            dataset = '1';
        }
        component.set('v.dataset', dataset)
        helper.setupChart(component);
    } 
})

(AndeeChartHelper.js)
({
    setupChart  : function(component) {


        // Normally call apex controller to get data but hardcoded for demonstration purposes
        var dataset = component.get('v.dataset');
        var data;
        var jsonRetVal
        if (dataset == '1'){
            jsonRetVal = {"chartLabels":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"chartData":[1.00,3.00,6.00,10.00,15.00,21.00]}
        } else {
           jsonRetVal = {"chartLabels":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"chartData":[21.00,3.00,16.00,19.00,17.00,12.00]} 
        }


        var el = component.find('andeeChart').getElement();
        var ctx = el.getContext('2d'); 

        // Need something here to destroy any chart that is currently being displayed to stop the 'flicker'

        new Chart(ctx, {
            type: 'bar',
            data: {
                labels: jsonRetVal.chartLabels,
                datasets: [
                    {
                        label: "Data",
                        fillColor: "rgba(220,220,220,1)",
                        strokeColor: "rgba(220,220,220,1)",                
                        data: jsonRetVal.chartData
                    }
                ]
            },
            options: {
                hover: {
                    mode: "none"
                },
                scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero:true
                        }
                    }]
                }
            }
        });



    }
})

Thanks in advance for any help you can offer as this is driving me made.
SticklebackStickleback
Thanks for looking at this but no, not move the chart but redisplay it with new data .  The scenario is that the data shown in the chart can vary depending upon what a user selects.  So in my code example above, the data shown in the chart is changed when a checkbox is checked/unchecked.  In a real world example this could be a dropdown from which a user selects a country & the chart then changes to show data relating to the selected country.  The problem seems to be that when the chart is rerendered with new data, the old chart is still underneath it & can re-appear when moving the mouse over the chart.  Everything about the lightning component is included in my original post.  The guidance from the chart.js website is to destroy the original chart object before reloading it with new data but I cannot find a way to do that from within lightning.
SticklebackStickleback
Update for anyone that comes along later.  Solution seems to be to create an attribute of type Object in the component & then in the javascript controller store the chart object in the attribute aftyer it has been created.  This object can then be checked to see if not null & destroyed if it is.  However as of Summer 16, Service Locker is automatically enabled on new orgs or orgs that don't have any lightning components & this causes an error when trying to destroy the old chart - TypeError: Cannot read property 'removeChild'.  Opening case with Salesforce Support.
Priyanka S 27Priyanka S 27
Hi,

Place the canvas tag inside any div tag. First time you can successfully draw the chart on the canvas .Next time when you redraw the chart on the same canvas you just clear the canvas space by deleting the parent node. Call to a different function when you redraw the chart on the same canvas.
For E.g)
                     <div id="chartDiv">
                            <canvas aura:id="largeChart" id="myChart" class="myChartLarge" />                            
                        </div>

            var itemNode = document.getElementById('myChart');
            itemNode.parentNode.removeChild(itemNode);
            document.getElementById('chartDiv').innerHTML = '<canvas id="myChart"></canvas>';

Note : Second time only the canvas portion has to be cleared as above

If you do like this every time the same canvas can be reused to draw the chart.

Regards,
Priyanka S
RemcoRemco
Hi,

I seem to have run into the exact same problem as described in the original post. Did you manage to resolve the issue in the end? Would love to hear how...
SticklebackStickleback
We solved it by building another Lightning Component which was a wrapper around the lightning compnent we wanted to display.  Easier to demo via an example :-

Wrapper.cmp :-

<aura:component access="global">
    
    <aura:attribute name="selectedTerritoryId" type="id"  description="The id of the territory that the user has selected."/>
    
    <aura:handler event="c:TerritorySelectListSelected" action="{!c.territorySelected}"/>
    
    <div>
    {!v.body}
    </div>
    
</aura:component>


WrapperController :-
({
    territorySelected : function(component, event, helper) {
        var selectedTerritoryId = event.getParam("selectedTerritoryId");
        component.set('v.selectedTerritoryId', selectedTerritoryId);
        
         $A.createComponent(
            "c:ShortTermIncentiveChart",
            {
                "selectedTerritoryId": selectedTerritoryId
            },
            function(newChartComp, status, errorMessage){
                if(status == "SUCCESS"){
                    var body = component.get("v.body");
                    body.pop();
                    body.push(newChartComp);
                    component.set("v.body", body);
                } 
            }
        );
    }
})

So in this case we have the picklist in a separate Lightning component. When a new picklist value is chosen it fires a lightning event which the wrapper is registerd to handle.  With the wrapper's controller it will destroy the existing body & then rebuild it with a new version of the 'real' lightning component that we wish to display.

Hope that makes some sort of sense.


 
RemcoRemco
Hi,

Thanks that does make sense!

Would you happen to have an example of how you called the Chart.js in the "c:ShortTermIncentiveChart" ? For some reaosn I can't get the charts to display through the "underlying" component.
SticklebackStickleback
Here you go :-

ShortTermIncentiveChart.cmp
<aura:component controller="ShortTermIncentiveChartController" implements="flexipage:availableForAllPageTypes" access="global">
    
    <aura:attribute name="selectedTerritoryId" type="id"  description="The id of the territory that the user has selected."/>
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <ltng:require scripts="{!$Resource.Chart}"/>
    <div class="slds-grid slds-wrap">
        <div aura:id="stiChartDiv" class="slds-col slds-size--1-of-1 slds-medium-size--1-of-1">
            <canvas aura:id="stiChart" id="stiChartId"/>
        </div>
    </div>
</aura:component>

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

ShortTermIncentiveChartHelper.js
({
    
    setupChart  : function(component, event, helper) {
        var selectedTerritoryId = component.get('v.selectedTerritoryId');
        var action = component.get("c.GetChartDataJSON");
        action.setParams({ "selectedTerritoryId" : selectedTerritoryId});
        action.setCallback(this, function(a){
            var jsonRetVal = JSON.parse(a.getReturnValue());
            
            var el = component.find('stiChart').getElement();
            var ctx = el.getContext('2d'); 
            new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: jsonRetVal.chartLabels,
                datasets: [
                    {
                        label: "Cumulative Gross Achieved",
                        fill: false,
                        backgroundColor: "rgba(11,111,206,0.8)",                    
                        data: jsonRetVal.chartActualData
                    },
                    {
                        type: 'line',
                        label: "Cumulative Gross Target",
                        fill: true,
                        borderColor: "rgba(164,188,152,1)",
                        backgroundColor: "rgba(184,208,172,0.4)",
                        pointBackgroundColor: "rgba(255,0,0,1)", 
                        data: jsonRetVal.chartTargetData
                    }
                    ]
                },
                options: {
                    hover: {
                        mode: "none"
                    },
                    tooltips : {

                        callbacks : { 
                            title : function() { 
                                return '';
                            },
                            beforeLabel : function(tooltipItem) {
                                if (tooltipItem.xLabel=='Jan') return 'January';
                                if (tooltipItem.xLabel=='Feb') return 'February';
                                if (tooltipItem.xLabel=='Mar') return 'March';
                                if (tooltipItem.xLabel=='Apr') return 'April';
                                if (tooltipItem.xLabel=='May') return 'May';
                                if (tooltipItem.xLabel=='Jun') return 'June';
                                if (tooltipItem.xLabel=='Jul') return 'July';
                                if (tooltipItem.xLabel=='Aug') return 'August';
                                if (tooltipItem.xLabel=='Sep') return 'September';
                                if (tooltipItem.xLabel=='Oct') return 'October';
                                if (tooltipItem.xLabel=='Nov') return 'November';
                                if (tooltipItem.xLabel=='Dec') return 'December';
                                return tooltipItem.xLabel;
                            },
                            label : function(tooltipItem, data) {
                                return data.datasets[tooltipItem.datasetIndex].label + ': ' + (Math.round(tooltipItem.yLabel*100)/100).toLocaleString() + 'm';
                            }
                        }
        
                    },
                    scales: {
                        xAxes: [{
                            scaleLabel: {
                                display: true,
                                labelString: 'Month',
                                fontstyle: 'bold',
                                fontSize: 20
                            }
                        }],
                        yAxes: [{
                            ticks: {
                                beginAtZero:true
                            },
                            scaleLabel: {
                                display: true,
                                labelString: 'Gross Sales (Millions)',
                                fontstyle: 'bold',
                                fontSize: 20
                            }
                        }]
                    }
                }
            });

            
        });
        $A.enqueueAction(action);


    }
})


ShortTermIncentiveChartController.cls
public class ShortTermIncentiveChartController {
    @AuraEnabled
    public static String GetChartDataJSON(Id selectedTerritoryId){
        
        decimal annualTarget;
        decimal runningTotalSales = 0;  
        string[] months = new String[]{'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'};


        for(region__c r : [select id, Annual_Sales_Target__c from region__c where id = :selectedTerritoryId limit 1]){ 
            annualTarget = r.Annual_Sales_Target__c;
        }
        
        ChartDataWrapper chartData = new ChartDataWrapper();
        for (ShortTermIncentiveValue__c rec: [select Sales_As_At__c, Total__c
                                              from ShortTermIncentiveValue__c
                                              where region__c = :selectedTerritoryId
                                              order by Sales_As_At__c
                                              limit 12]){
            integer month = rec.Sales_As_At__c.month();
            chartData.chartLabels.add(months[month-1]);
            
            chartData.chartTargetData.add(((annualTarget == null ? 0 : annualTarget) / 12 * month) / 1000000);      //Show in millions
            if (rec.Total__c != null){
                runningTotalSales = runningTotalSales + rec.Total__c; 
                chartData.chartActualData.add(runningTotalSales / 1000000);      //Show in millions
            }
        }
        return System.json.serialize(chartData);

    }  


    class ChartDataWrapper
    {
        public List<String> chartLabels {get;set;}
        public List<Decimal> chartTargetData {get;set;}
        public List<Decimal> chartActualData {get;set;}
       
        public ChartDataWrapper(){
            chartLabels = new List<String>();
            chartTargetData = new List<Decimal>();
            chartActualData = new List<Decimal>();
        }
    }

}
RemcoRemco
Great, thank you very much. Got it to work!
SticklebackStickleback
Good to hear!
Prateek Jain 67Prateek Jain 67
Hi Stickleback

I tried implementing your way, but I am continously getting the following error on page load "Error in $A.getCallback() [Chart is not defined] Failing descriptor: {markup://c:SPerformanceChart"}. Do you have any idea what could be the issue.

Thanks
Prateek