How to avoid detached HTMLCanvasElements when updating canvas with JavaScript? - javascript

So my problem is that when I'm cyclically updating canvas element in my HTML with line chart created by Chart.js library I'm getting bunch of detached HTMLCanvasElements. I noticed this when I was fixing some memory leaks in my code which earlier lead my webpage to crash(chrome gave "aw snap" error page).
I was able to fix most of the memory leaks but this one is still bothering me and I am helpless because of not knowing why this is happening..
I have canvas elements in my HTML code like this:
<div class="kuvaaja"><canvas id="etaisyyskuvaaja"></canvas></div>
<div class="kuvaaja"><canvas id="etaisyyskuvaaja2"></canvas></div>
<div class="kuvaaja"><canvas id="etaisyyskuvaaja3"></canvas></div>
<div class="kuvaaja"><canvas id="etaisyyskuvaaja4"></canvas></div>
And this is how I fetch those elements in my JavaScript (in window.onload function):
pohjacanvas = document.getElementById("etaisyyskuvaaja");
pohjacanvas2 = document.getElementById("etaisyyskuvaaja2");
pohjacanvas3 = document.getElementById("etaisyyskuvaaja3");
pohjacanvas4 = document.getElementById("etaisyyskuvaaja4");
Then I start cyclic updating of those canvases with(also inside window.onload):
paivitysvali = setInterval(haeetaisyysmittaukset, 1000);
painepaivitys = setInterval(haepainemittaukset, 1000);
And in these functions I first fetch data from database like this (haepainemittaukset() is similar to this one just different data is fetched):
function haeetaisyysmittaukset() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
etaisyys = [];
ajat = [];
var mittaukset = JSON.parse(xmlhttp.response);
for (var i = 0; i < mittaukset.length; i++) {
etaisyys.push(mittaukset[i]["etaisyys"]);
ajat.push(mittaukset[i]["timestamp"]);
}
if (paivitysbitti == 1) {
etaisyys.reverse();
ajat.reverse();
luokuvaaja2(pohjacanvas, etaisyys, ajat);
luokuvaaja2(pohjacanvas4, etaisyys, ajat);
}
}
}
xmlhttp.open("POST", "haeetaisyysmittaukset.php", true);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp.send("limit=" + mittausmaara);
}
After data is fetched in global arrays (etaisyys[] and ajat[]) I call luokuvaaja2() function with the canvas I want to update and these data arrays. And this is the phase where problem starts at least I think so.. Here is how I am trying to implement this update of canvas:
function luokuvaaja2(pohja, data, ajat) {
pohja.height = 400;
pohja.width = 700;
myChart = new Chart(pohja, {
type: 'line',
data: {
labels: ajat,
datasets: [{
data: data,
label: "Etaisyys",
borderColor: "blue",
fill: false
}
]
},
options: {
animation: {
duration: 0, // general animation time
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
title: {
display: true,
text: 'Etaisyysmittaus'
},
scales: {
yAxes: [{
ticks: {
min: 0,
max: 6000
}
}]
}
}
});
}
So what I think that I'm doing here is setting canvas height and width, after that I create new chart in the same canvas where old one was(or am I?). However it looks like that there is something wrong and my current code is creating bunch of detached canvas elements but I can't understand why and where this is exactly happening? So now memory footprint of my page is slowly increasing after every cycle (canvas update).
Here is also snapshot of memory distribution:
memory distribution
In this memory distribution there are many detached canvas elements all pointing to those canvases(pohjacanvas, pohjacanvas2, pohjacanvas3, pohjacanvas4).

You should keep track (in an array, for example) of your charts previously created, then when the new data is fetched from the server, you need to retrieve the chart to update, update its data and/or options, and then call
chart.update();
to refresh the chart. In this case you won't create new charts at each update.
You may find more details in the Chart.js docs.

Related

Instead of creating new chart in ChartJS, the new updated chart keeps the old data and adds the new

I have been trying to solve this problem with ChartJS for a few days now, and I am completely stumped
My program shows the user a set of input elements they use to select data needing to be charted, plus a button that has an event to chart their data. The first chart works great. If they make a change to the data and click the button a second, third, or more time, all the data from the previous charts is plotted, PLUS their most recent selection.
It is behaving exactly like you might expect if the chart.destroy() object is not working, or perhaps would work if I created the chart object using a CONST (and could therefore add new data but not delete the beginning data).
I have tried all combinations of the browsers, chartjs and jquery libraries below:
Three different browsers:
• Chrome: Version 107.0.5304.121 (Official Build) (64-bit)
• Microsoft Edge: Version 107.0.1418.56 (Official build) (64-bit)
• Firefox: 107.0 64-bit
I have tried at least three different versions of Chart.js, including
• Versions 3.9.1
• 3.6.2
• 3.7.0
Jquery.js
• v3.6.1
• v1.11.1
Other things I have tried:
"use strict" (no luck)
In addition to destroying the chart object, removed the div containing the canvas, and appending it again.
using setTimeout() function before updating the chart after destroying it (because I thought maybe giving the destroy method more time might help)
type here
Software:
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/chart.js"></script>
<script type="text/javascript" src="js/dropdownLists.js"></script>
<script type="text/javascript" src="js/chartDataFunctions.js"></script>
<script type="text/javascript" src="js/chartJSFunctions.js"></script>
<body>
<div class = metadatasetup4" id = "buttons">
<button class="download" id="getchart" value="Get Chart">Chart</button>
<button class="download" id="downloadchart" value="Download">Download</button>
</div>
<div id = "bigchartdiv" class="bigchart">
<canvas id="myChart"></canvas>
</div>
</body>
<script>
$(window).on('load',function(){
//NOTE 1: In of my attempts to troubleshoot I tried strict mode (it didn't work)
//"use strict";
let data = {
labels: lbl,
datasets: [
]
};
let config = {
type: 'line',
data: data,
options: {
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
min:0,
pointStyle:'circle',
},
y1: {
type: 'linear',
display: true,
position: 'right',
suggestedMax: 25,
min: 0,
pointStyle: 'cross',
// grid line settings
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
}
}
};
// NOTE 2: The next line below, beginning with "var bigChartHTML =" was one of my later attempts to
// solve the problem. It didn't work, but my thought process was that if I removed
// the div containing the canvas, AND destroyed the chart object, that appending a "fresh"
// chart div to the body might be a work-around. This did not work.
var bigChartHTML = '<div id = "bigchartdiv" class="bigchart"><canvas id="myChart"></canvas></div>'
let ctx = document.getElementById('myChart').getContext('2d');
let bigChart = null;
// The getChartData() function below uses Ajax to populate various dropdown lists
// which enable the user to select the data is to be charted.
// There are no chartjs-related operations in getChartData()
getChartData();
$('#buttons').on('click','#getchart',function(){
if (bigChart!=null) {
//removeData(bigChart);
bigChart.destroy();
//bigChart = 1;
}
$("#bigchartdiv").empty(); //for this and next 2 lines, see NOTE 2 above
$("#bigchartdiv").remove();
$(bigChartHTML).insertAfter("#chartcontrols");
bigChart = new Chart(document.getElementById('myChart'),config);
//NOTE 3: I thought maybe bigChart.destroy() took time, so I tried
// using the setTimeout function to delay updating the chart
// (didn't work, but I left it in the code, anyway.)
setTimeout(function() {updateChart(bigChart)}, 2000);
//updateChart(bigChart);
});
// NOTE: The updateChart() function is actually included in "js/chartDataFunctions.js"
function updateChart(chart) {
/*
This section of the program reads the HTML elements then uses them
to make an Ajax request to sql server, and these become the
parameters for the newDataSet() function below.
*/
newDataset(chart,firstElement,newdataset,backgroundcolor,color);
}
// NOTE: The newDataSet() function is actually included in "js/chartJSFunctions.js"
// I show it here for brevity.
// It decides which axis (y or y1) to use to plot the datasets
// the dataset is pushed into the data, and chart.update() puts it in the chart object
function newDataset(chart,label,data,bgcolor='white',color='rgb(255,255,255)') {
var maxValue = Math.max(...data);
if (Number.isNaN(maxValue)) {
return;
}
if (maxValue == 0) {
return;
}
var axisID = 'y';
var ptStyle = 'circle';
//var pStyle = 'circle';
if (maxValue < 50) {
axisID = 'y1';
bgcolor = 'white';
//ptStyle = 'Star'
}
chart.data.datasets.push({
label:label,
yAxisID:axisID,
data:data,
borderColor:color,
backgroundColor:bgcolor,
//pointStyle:ptStyle
});
chart.update();
}
});
</script>
I found a work-around that solves my problem, but I still think this is a bug in ChartJS. Before calling bigChart.destroy(), I now do two things: First, reset the data object back to it's original value, and second, reset the config object back to it's original value, THEN call bigChart.destroy().
I think the destroy() method should handle that for me, but in my case, for whatever reason, it doesn't.
So, what I have is a work-around, not really a solution, but I'll take it.

how do I clear a apache Echart? I need to re-initialize it

I have Stacked Horizontal Bar chart on a single page, and each chart changes based on what the user selects (Drop Down Select). I have an ajax call that retrieves the data, so the data varies and is dynamic based on user selection. I'm having a problem clearing out old data. If there is no data it should display Horizontal Bar chart. But it displays previous data. it is not getting empty if there is no data. Basically, I just want after each selection to re-initialize the chart and start from scratch. How can I do that?
<script type = "text/javascript" >
var series;
$("#sub_project3").change(function() {
$.ajax({
url: "<?php echo base_url("
Manage_procurement_plan / load_bar_chart ");?>",
type: "POST",
data: {
drop_value: $(this).val()
},
dataType: "text",
cache: false,
success: function(data) {
series = data;
var dom = document.getElementById("main");
var myChart = echarts.init(dom);
var app = {};
var option;
getBarGraph(series);
function getBarGraph(data) {
option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
top: '3%',
},
grid: {
top: '28%',
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
data: ['Actual Avg', 'ADB Min Standard']
},
series: JSON.parse(data),
};
/*if (option && typeof option === 'object') {
myChart.setOption(option);
}*/
myChart.setOption(option);
}
}
});
});
</script>
Each time you make a new selection, you can 're-initialize' your series (make it an empty list) before pushing new data inside it. So if there is no data, the series will remain empty.
Then, you can update your chart using notMerge = true. According to echarts doc :
notMerge Optional. Whether not to merge with previous option. false by
default, means merge, see more details in Component Merging Modes. If
true all of the current components will be removed and new components
will be created according to the new option.
myChart.setOption(option, true);
I had the same problem today.
What I see in your question is that you are using each Ajax call to create a new chart.
var dom = document.getElementById("main");
var myChart = echarts.init(dom);
So that Ajax call is fine for the very first time you create the chart and show the initial data.
The fact is that the Apache Echarts library remembers each drawn chart on it's instances. So, on every call of this success method you are actually creating a new chart.
So, you must create another Ajax call for updating the data, where first, you must find the chart instance, and execute the setOption method, as well said by #A mere dev in the other answer. On the only update Ajax call, you'll don't need to create a new chart, but just receive the new data and pass it
myChart.setOption(option, true);

Resizing a responsive DataTable is very slow, how can I speed it up?

I have a DataTable which has a couple of thousand records in it.
I have the responsive plugin for it, and the responsive option is enabled.
I also tried enabling the deferRender option, but this appeared to have no impact on the time taken.
When I resize the browser there is a delay of 1s - 2s. This happens in IE11, and MS Edge. The performance in chrome isn't fantastic, but at 0.5s it's tolerable.
I am using custom ordering functions, but those functions are omitted for brevity. I'm fairly sure I know where the issue is, and it's not in them. I can provide them if required.
Here's my initialisation code:
this._dataTable = $("#listtable").DataTable({
paging: true,
responsive: true,
deferRender: true,
columns: [{
title: "Name",
data: "thing.name"
}, {
title: "State 1",
data: "state1",
type: "state1",
render: (data, type, row, meta) => {
return this._renderState1(data, meta);
}
}, {
title: "State 2",
data: "state2",
type: "state2",
render: (data, type, row, meta) => {
return this._renderState2(data, meta);
}
}]
});
I load the data by calling dataTable.row.add for each item, and then calling dataTable.draw at the end.
The performance issues occur after all the data has been successfully loaded, so I don't think it's to do with that.
Digging further in to the profiler information I found that it was the rendering of the rows that was the issue:
By commenting out code in my custom render functions shown in the initalisation code, I found that the issue lay with finding the containing cell to set the background colour:
var cell = this._dataTable
.cell({ row: meta.row, column: meta.col })
.node();
Here's the rest of the code for setting the background colour:
var cellClass = this._getStateClass(state);
$(cell).addClass(cellClass);
If I comment the cell retrieval line out then the performance isn't amazing, but it is acceptable.
So my question is how can I have a custom background colour for cells while maintaing the responsive performance?
A fast alternative to dataTable.cell would do, as would an alternative approach to setting the background colour.
I managed to solve this issue by removing the need to find the cell.
I put a class on the columns that removed the padding they have.
Style:
.cell-state1 {
padding: 0;
}
Configuration:
this._dataTable = $("#listtable").DataTable({
paging: true,
responsive: true,
deferRender: true,
columns: [{
title: "Name",
data: "thing.name"
}, {
title: "State1",
data: "state1",
type: "state1",
className: "cell-state1",
render: (data, type, row, meta) => {
return this._renderState1(data, meta);
}
}]
});
Then I changed my render functions so they returned the content in a div which filled the cell, had the background colour, and added the padding back in.
Style:
.cell-state1-somestate {
height: 100%;
width: 100%;
padding: 8px;
background-color: #000000;
color: #ffffff;
}
Render Function:
function _renderState1 (state1) {
var cssClass = _this._getState1CellClass(state1);
var text = _this._getState1CellText(state1);
var content = "<div class='" + cssClass + "'>" + text + "</div>";
return content;
};
This left me with one final issue.
I have custom order functions, and now rather than being passed the text value they're passed the div containing the text value.
I used a little bit of jQuery to extract the text:
var floodAlertSeverity = $(content).html();.
It'd be nice if the order functions received the original data, rather than the rendered data, but oh well.
I had a similar issue with IE11 becoming extremely slow when resizing the browser window in responsive mode, which made for a pretty terrible user experience.
I don't have the time nor the expertise to fix the underlying issue (probably just IE11 being slow), but I came up with an elegant hack to work around the problem, which is basically to throttle the calls to the function that adjusts the column sizes.
The performance profiler in IE11 showed that calls to _fnAdjustColumnSizing(oSettings); were taking most of the CPU time, and the calls to this method are triggered by the 'resize.DT-YourTableNameHere' event, so using a simple timer we can delay the call to this function until the user is done resizing the window:
var dtResizeTimer;
var allowPropagation = false;
$(window).on("resize.DT-visitsTable", function (event) {
if (allowPropagation === false) {
event.stopImmediatePropagation();
clearTimeout(dtResizeTimer);
dtResizeTimer = setTimeout(function() {
allowPropagation = true;
$(window).trigger("resize.DT-visitsTable");
}, 100);
} else {
allowPropagation = false;
}
});
Obviously you'll need to replace visitsTable with whatever id you gave to your table element.
It still takes a second for IE11 to update the table after the user is done resizing, but at least the resizing itself is now smooth. There might be a better solution, but for now, this calms my frustration with IE11.

chart is not rendering properly, canvasjs

I'm trying to render two dynamic charts using canvasJS, Chart one is rendering properly, in fact second chart is populating properly, please see the inspect element image, but it's not displaying on the page.
This is interface, just one chart is displaying
and here is inspect elem.
#adverts is rendering, but not displayed here.
.chartContainer{
height: 400px;
width: 100%;
}
PS I tried to commented out first chart rendering code, but not working at all, still displaying the first one only.
EDIT
Here is console log about what I'm getting back to render chart
here is snippet
function drawChart(obj, placeholder, From, To){
var legendFrom;
var legendTo;
if(From != 'undefined'){
legendFrom = From;
}
if(To != 'undefined'){
legendTo = To;
}
var dataPoints = [];
var from = [];
var to = [];
var advertFrom = [];
var advertTo = [];
for (var i = 0; i <= obj.length - 1; i++) {
if(obj[i].typeOf != 'undefined' || obj[i].typeOf != ''){
if(obj[i].typeOf == 'from'){
from.push({label:obj[i].year+"-"+obj[i].month+"-"+obj[i].day,y:Math.round(obj[i].avgPrice * 1000) / 1000});
advertFrom.push({label:obj[i].year+"-"+obj[i].month+"-"+obj[i].day,y:obj[i].adverts});
}else{
to.push({label:obj[i].year+"-"+obj[i].month+"-"+obj[i].day,y:Math.round(obj[i].avgPrice * 1000) / 1000});
advertTo.push({label:obj[i].year+"-"+obj[i].month+"-"+obj[i].day,y:obj[i].adverts});
}
}
else
from.push({label:obj[i].year+"-"+obj[i].month+"-"+obj[i].day,y:Math.round(obj[i].avgPrice * 1000) / 1000});
}
var chart = new CanvasJS.Chart("chartContainer",
{
animationEnabled: true,
//theme: "theme1",
zoomEnabled: true,
title:{
text: placeholder
},
data: [
{
type: "spline", //change type to bar, line, area, pie, etc
showInLegend: true,
legendText: "From "+legendFrom,
dataPoints: from
},
{
type: "spline",
showInLegend: true,
legendText: "From "+legendTo,
dataPoints: to
}
],
legend: {
cursor: "pointer",
itemclick: function (e) {
if (typeof(e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
e.dataSeries.visible = false;
} else {
e.dataSeries.visible = true;
}
chart.render();
}
}
});
chart.render();
var adverts = new CanvasJS.Chart("adverts",
{
animationEnabled: true,
zoomEnabled: true,
title:{
text: placeholder
},
data: [
{
type: "column", //change type to bar, line, area, pie, etc
showInLegend: true,
legendText: "From "+legendFrom,
dataPoints: advertFrom
},
{
type: "column",
showInLegend: true,
legendText: "From "+legendTo,
dataPoints: advertTo
}
]
});
adverts.render();
}
I just replicated a canvasJS enviroment in JSFiddle
http://jsfiddle.net/fLbngdv9/1/
Make sure you are are using a new VAR for each chart.
var chart = new CanvasJS.Chart("chartContainer", {..});
chart.render();
var chart2 = new CanvasJS.Chart("chartContainer2", {..});
chart2.render();
Seems to work fine. If you can provide me with some code examples then perhaps I can help you find an answer.
Probably just a copy paste mistake but you have a syntax error at the very bottom.
...
adverts.render();
}
This should be
...
adverts.render();
Just remove the extra bracket. I updated the JS Fiddle to be more like your example... [here]
Edit:
I was able to replicate in my latest JSFiddle and I know this sounds crazy but when I changed the ID of your second div from adverts to chartContainer2 it worked fine.
Still investigating as to why this is but I would try renaming the ID on the div and also the reference for the graphs creation.
SEE HERE
Edit 2:
This appears to be a Chrome specific bug with the ID adverts. To replicate all you need to do is check out this link http://www.briangebel.com/test.html
(As you can see the div Adverts is hidden in Chrome but visible on FF,Opera,IE)
So what is happening..
Chrome's ADBlocker extension sees this ID and automatically adds an in page stylesheet.
This style sheet has the following Style
This only affects Chrome as it is caused by a specific extension. (I am attempting to contact the developer to see if he can resolve this but until then follow the below solution)
Solution
Simple don't use that ID. (You could simply disable AdBlocker but this extension is so widely used I would still recommend changing the ID)

How to color Google Column Chart's every bar differently and keep them as it is

Though I have successfully colored the bars of google chart individually but not able to keep them when we hover mouse over it. It is getting reset back to blue(which is default).
Here is the jsfiddle of what I have done jsfiddle.
I tried to control the hover behaviour with multiple ways like below.
This I am keeping outside (document.ready) but inside script tag.
1)
$('#chart_div').hover(
function() {
$('#chart_client').hide(); // chart_client is another google chart div.
}, function() { // just for testing I was doing hide/show of that.
$('#chart_client').show();
}
);
2)
$("#chart_div").on({
mouseenter: function () {
$('#chart_client').hide();
},
mouseleave:function () {
$('#chart_client').show();
}
},'rect');
3)
google.visualization.events.addListener('#chart_div', 'ready', function () {
$('#chart_div rect').mouseover(function (e) {
alert('hello');
});
});
I must be doing something wrong, could you please tell me what and where.
I solved it using below code. Earlier I was trying to create charts using dynamically adding rows into chart(please visit my jsfiddle) but with this below approach I am first preparing data(converting dynamic to static) and adding that static data in to chart's 'arrayToDataTable' method.
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawUserKeywordChart);
function drawUserKeywordChart() {
var val = 'Tax:47;Finance:95;Awards:126;Bank:137;Debt:145;';
var length = val.length;
var array = [];
//preparing data
while(length>0){
var sepAt = val.indexOf(";");
var value = parseInt(val.substring(val.indexOf(":")+1, sepAt));
array.push(val.substring(0, val.indexOf(":")));
array.push(value);
val = val.substring(val.indexOf(";")+1, length);
length = val.length;
}
var data = google.visualization.arrayToDataTable([
['Keyword', 'Occurences', { role: 'style' }],
[array[0], array[1], '#8AA3B3'],
[array[2], array[3], '#A9B089'],
[array[4], array[5], '#848C49'],
[array[6], array[7], '#44464A'],
[array[8], array[9], '#704610'],
]);
var options = {
title: 'Keyword Matches',
width: 660,
height: 450,
titleTextStyle:{bold:true,fontSize:20},
legend:{position:'none'}
};
var chart = new google.visualization.ColumnChart(document.getElementById('chart_keyword1'));
chart.draw(data, options);
}
Please advice if you find anything wrong here or you have better approach than this.

Categories