You need to sign in to do that
Don't have an account?
Stickleback
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)
(AndeeChartHelper.js)
Thanks in advance for any help you can offer as this is driving me made.
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.
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
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...
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.
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.
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>();
}
}
}
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