How to make a dynamically growing data chart in Chart.JS? - javascript

I am trying to develop a Crash Game, where a multiplier (Y) increases exponentially and dynamically over time (X), causing the chart to re-render at each tick.
You can see an example of the chart game here
TL;DR: I am trying to achieve a "zoom-out" effect of the chart as my ticks increase in values (x,y).
Where my code fails is when ticks data values (x,y, respectively time and multiplier) surpass suggestedMax tick values. The only reason I am using suggestedMax is to have some labels on the chart at the beginning.
I have tried to achieve this by using both line and scatter chart type, but the final outcome is simply unacceptable from a performance point of view.
Here is my code:
const HomePlaygroundView = () => {
var chart = undefined;
const chartText = useRef(null);
let last_tick_received = 0;
const incrementChart = () => {
last_tick_received += 100;
};
const onServerTickReceived = (multiplier, msLapsed) => {
// Update chart multiplied
if (chart.data.datasets[0].data.length >= 100) {
// Halve the array to save performance (lol)
for (let i = 1; i < 100; i += 2) {
console.log("Reducing chart data");
chart.data.datasets[0].data.splice(i, 1);
}
}
chart.data.datasets[0].data.push({
x: msLapsed,
y: multiplier,
});
// This is basically my zoom out effect implementation...
if (multiplier >= 2.5) { // Increase suggestedMax only if bigger data needs to be fit
chart.options.scales.yAxes[0].ticks.suggestedMax = multiplier;
}
if (msLapsed > 9000) { // Same logic as above
chart.options.scales.xAxes[0].ticks.suggestedMax = msLapsed;
}
if (msLapsed < 10000) {
// Fit msLapsed in the pre-existing 10 seconds labels of x axis (this is a hell of a workaround)
let willInsertAtIndex = undefined;
for (let i = 0; i < chart.data.labels.length; i++) {
let current = chart.data.labels;
if (current < msLapsed) {
// Insert at i + 1? Check the next index if it's bigger than msLapsed
let nextVal = chart.data.labels[i + 1];
if (nextVal) {
if (nextVal > msLapsed) {
willInsertAtIndex = i + 1;
break;
}
} else {
willInsertAtIndex = i + 1;
break;
}
}
}
if (willInsertAtIndex) {
chart.data.labels.splice(willInsertAtIndex, 0, msLapsed);
}
} else {
chart.data.labels.push(msLapsed);
}
// Decimate data every so and so
chartText.current.innerText = `${multiplier}x`;
// Re-render canvas
chart.update();
};
useEffect(() => {
console.log("rendered chart");
var ctx = document.getElementById("myChart").getContext("2d");
ctx.height = "350px";
chart = new Chart(ctx, {
// The type of chart we want to create
type: "scatter",
// The data for our dataset
data: {
labels: [...Array(11).keys()].map((s) => s * 1000),
datasets: [
{
label: "testt",
backgroundColor: "transparent",
borderColor: "rgb(255, 99, 132)",
borderWidth: 10,
showLine: true,
borderJoinStyle: "round",
borderCapStyle: "round",
data: [
{
y: 1,
x: 0,
},
],
},
],
animation: {
duration: 0,
},
responsiveAnimationDuration: 100, // animation duration after a resize
},
// Configuration options go here
options: {
spanGaps: true,
events: [],
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
elements: {
point: {
radius: 0,
},
},
scales: {
xAxes: [
{
type: "linear",
ticks: {
callback: function (value, index, values) {
let s = Math.round(value / 1000);
return s.toString() + "s";
//return value;
},
autoSkipPadding: 100,
autoSkip: true,
suggestedMax: 10000,
stepSize: 100,
min: 0,
},
},
],
yAxes: [
{
ticks: {
// Include a dollar sign in the ticks
callback: function (value, index, values) {
return Math.round(value).toString() + "x"; // Display steps by 0,5
},
min: 1,
suggestedMax: 2.5,
stepSize: 0.01,
autoSkip: true,
autoSkipPadding: 150,
},
},
],
},
},
});
let lastTick = 1.0;
let dateStart = new Date().getTime();
setTimeout(() => {
chartText.current.innerText = "Go!";
setTimeout(() => {
setInterval(() => {
let timePassed = new Date().getTime() - dateStart;
//console.log(timePassed);
let calculateTick = Math.pow(
1.01,
0.00530133800509 * timePassed
).toFixed(2);
console.log(timePassed);
onServerTickReceived(calculateTick, timePassed);
}, 50);
}, 1000);
}, 2000);
});
const classes = useStyles();
return (
<div className={classes.canvasContainer}>
<span ref={chartText} className={classes.canvasText}>
Ready...?
</span>
<canvas id="myChart"></canvas>
</div>
);
};
export default HomePlaygroundView;

Related

chart.js doughnut chart with links if onclick

I try to make a link if a onclick event on a doughnut chart slice happens. My datasources are 3 arrays with labels, value, and the id for the url.
HTML:
<canvas id="pie-chart" style='display: none;'></canvas>
<!-- Php Arrays to JS -> PIE-CHARTDATA -->
<script type="text/javascript">
var chartIds = [[12,14,17,18]];
var chartValues = [[208.09,296.86,634.975,972.808]];
var chartLabels = [["BTC","AAPL","MSFT","ETH"]];
</script>
JS:
if (chartValues.length != 0 ) {
document.getElementById("pie-chart").style.display= "block";
}
Chart.register(ChartDataLabels);
var chartValuesInt = [];
length = chartValues[0].length;
for (var i = 0; i < length; i++)
chartValuesInt.push(parseInt(chartValues[0][i]));
var data = [{
data: chartValuesInt,
chartIds,
backgroundColor: [
"#f38000",
"#5f44f5",
"#333333",
],
borderColor: "#000"
}];
var options = {
borderWidth: 4,
hoverOffset: 6,
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false,
},
datalabels: {
formatter: (value, ctx) => {
let sum = 0;
let dataArr = ctx.chart.data.datasets[0].data;
dataArr.map(data => {
sum += data;
});
let percentage = (value*100 / sum).toFixed(2)+"%";
return [ctx.chart.data.labels[ctx.dataIndex],
percentage,
'$' + value ] ;
},
textAlign: 'center',
color: '#fff',
borderRadius: 50,
padding:10,
labels: {
title: {
font: {
weight: 'bold',
size: '16px'
}
},
}
}
},
options:{
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
console.log("In click", datasetLabel, value);
//link to url with:[chartIds]
}
}
};
//IMAGE CENTER
const image = new Image();
image.src = 'img/pie-home2.png';
const plugin = {
id: 'custom_canvas_background_image',
beforeDraw: (chart) => {
if (image.complete) {
const ctx = chart.ctx;
const {top, left, width, height} = chart.chartArea;
const x = left + width / 2 - image.width / 2;
const y = top + height / 2 - image.height / 2;
ctx.drawImage(image, x, y);
} else {
image.onload = () => chart.draw();
}
}
};
var ctx = document.getElementById("pie-chart").getContext('2d');
var myChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: chartLabels[0],
datasets: data,
chartIds
},
options: options,
plugins: [plugin],
});
why does the onclick didn't work ?
how do i get the id with the right index from the slice where the event happens?
I searched already, but couldn't find a answer to these 2 questions.
You onClick function does not work because you define an options object within your options object and put the onClick in there. This is not supported. When you remove the inner options layer it will work:
const options = {
borderWidth: 4,
hoverOffset: 6,
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false,
},
datalabels: {
formatter: (value, ctx) => {
let sum = 0;
let dataArr = ctx.chart.data.datasets[0].data;
dataArr.map(data => {
sum += data;
});
let percentage = (value * 100 / sum).toFixed(2) + "%";
return [ctx.chart.data.labels[ctx.dataIndex],
percentage,
'$' + value
];
},
textAlign: 'center',
color: '#fff',
borderRadius: 50,
padding: 10,
labels: {
title: {
font: {
weight: 'bold',
size: '16px'
}
},
}
}
},
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
console.log("In click", datasetLabel, value);
//link to url with:[chartIds]
}
};

Chart.js (chartjs-node-canvas) create date-based floating bar graph

I will attempt to explain my issue as clearly as possible while also avoid making this topic too long. I recently found the Chart.js library, which is excellent for what I need. Now, since I am using Node.js and need a png of the graph, I am utilizing the chartjs-node-canvas library. Having this information in mind, I will try to split my topic into multiple sections for a clearer understanding.
Ultimate Goal
Before getting into the problem itself, I would like to discuss my ultimate goal. This is to give a general idea on what I'm trying to do so the responses are fitted accordingly. To keep this short, I have data in the form of {awardedDate: "2022-06-22T12:21:17.22Z", badgeId: 1234567}, with awardedDate being a timestamp of when the badge was awarded, and the badgeId being the ID of the badge that was awarded (which is irrelevant to the graph, but it exists because it's part of the data). Now, I have a sample with around 2,787 of these objects, with all having different award dates and IDs, and with dates ranging from 2016 to 2022. My objective is to group these badges by month-year, and that month-year will have the amount of badges earned for that month during that year. With that data, I then want to make a waterfall graph which is based on the amount of badges earned that month of that year. As of right now, there isn't a specific structure on how this will look like, but it could range from an object that looks like {"02-2022": 10, "03-2022": 5} to anything else. I can of course restructure this format based on what is required for a waterfall graph.
Actual Questions
Now that you have a general idea of what my ultimate goal is, my actual question is how I'd be able to make a floating (we can leave the waterfall structure stuff for another topic) bar graph with that data. Since the data can have blank periods (it is possible for a dataset to have gaps that are months long), I cannot really utilize labels (unless I am saying something wrong), so an x-y relation works the best. I tried using the structure of {x: "2022-06-22T12:21:17.226Z", y: [10, 15]}, but that didn't really yield any results. As of right now, I am using a sample code to test how the graph reacts with the data, and of course I'll replace the test values with actual values once I have a finished product. Here is my code so far:
const config = {
type: "bar",
data: {
datasets: [{
label: "Badges",
data: [
{
x: "2022-06-22T12:41:17.226Z",
y: [10, 15]
}
],
borderColor: "rgb(75, 192, 192)",
borderSkipped: false
}]
},
options: {
plugins: {
legend: {
display: false
},
title: {
display: true,
text: "Test",
color: "#FFFFFF"
}
},
scales: {
x: {
type: 'time',
title: {
display: true,
text: 'Time',
color: "#FFFFFF"
},
min: "2022-06-22T12:21:17.226Z",
max: "2022-06-22T14:21:17.226Z",
grid: {
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
ticks: {
color: "#FFFFFF"
}
},
y: {
title: {
display: true,
text: 'Number of Badges',
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
min: 0,
max: 50,
grid: {
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
ticks: {
color: "#FFFFFF"
}
}
}
},
plugins: [
{
id: 'custom_canvas_background_color',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = '#303030';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
}
]
};
const imageBuffer = await canvasRenderService.renderToBuffer(config)
fs.writeFileSync("./chart2.png", imageBuffer)
And this is the graph that the code produces:
What is supposed to happen, of course, is that a float bar should be generated near the start that ranges from 5 to 10, but as seen above, nothing happens. If someone could assist me in my problem, that would be amazing. Thank you very much for your time and help, I greatly appreciate it.
Inspired by this answer, I came up with the following solution.
const baseData = [
{ awardedDate: "2022-06-22T12:21:17.22Z" },
{ awardedDate: "2022-06-18T12:21:17.22Z" },
{ awardedDate: "2022-06-15T12:21:17.22Z" },
{ awardedDate: "2022-05-20T12:21:17.22Z" },
{ awardedDate: "2022-05-10T12:21:17.22Z" },
{ awardedDate: "2022-04-16T12:21:17.22Z" },
{ awardedDate: "2022-04-09T12:21:17.22Z" },
{ awardedDate: "2022-04-03T12:21:17.22Z" },
{ awardedDate: "2022-04-01T12:21:17.22Z" },
{ awardedDate: "2022-02-18T12:21:17.22Z" },
{ awardedDate: "2022-02-12T12:21:17.22Z" },
{ awardedDate: "2022-01-17T12:21:17.22Z" }
];
const badgesPerMonth = baseData
.map(o => o.awardedDate)
.sort()
.map(v => moment(v))
.map(m => m.format('MMM YYYY'))
.reduce((acc, month) => {
const badges = acc[month] || 0;
acc[month] = badges + 1;
return acc;
}, {});
const months = Object.keys(badgesPerMonth);
const labels = months.concat('Total');
const data = [];
let total = 0;
for (let i = 0; i < months.length; i++) {
const vStart = total;
total += badgesPerMonth[months[i]];
data.push([vStart, total]);
}
data.push(total);
const backgroundColors = data
.map((o, i) => 'rgba(255, 99, 132, ' + (i + (11 - data.length)) * 0.1 + ')');
new Chart('badges', {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Badges',
data: data,
backgroundColor: backgroundColors,
barPercentage: 1,
categoryPercentage: 0.95
}]
},
options: {
plugins: {
tooltip: {
callbacks: {
label: ctx => {
const v = data[ctx.dataIndex];
return Array.isArray(v) ? v[1] - v[0] : v;
}
}
}
},
scales: {
y: {
ticks: {
beginAtZero: true,
stepSize: 2
}
}
}
}
});
<script src="https://rawgit.com/moment/moment/2.2.1/min/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.min.js"></script>
<canvas id="badges" height="95"></canvas>
If you also want to see the gaps, you would first have to initialize badgesPerMonth with following months between the earliest and latest date, each with value zero. Please take a look at this answer to get an idea about how this could be done.
After reading #uminder's reply, I was able to create the following code which solved my problem:
dateGroups = Object.fromEntries(
Object.entries(dateGroups).sort(([d1,],[d2,]) => {return (d1 < d2) ? -1 : ((d1 > d2) ? 1 : 0)})
)
const dateTimesConst = Object.keys(dateGroups)
const dateValuesConst = Object.values(dateGroups)
let dateTimes = []
let dateValues = []
let prevLength = 0
let mostBadgesPerMonth = 0
for (let i = 0; i < dateValuesConst.length; i++) {
const currentMonth = new Date(Date.parse(dateTimesConst[i]))
const previousMonth = new Date(Date.UTC(currentMonth.getUTCFullYear(), currentMonth.getUTCMonth() - 1, 1, 0, 0, 0, 0)).toISOString()
const nextMonth = new Date(Date.UTC(currentMonth.getUTCFullYear(), currentMonth.getUTCMonth() + 1, 1, 0, 0, 0, 0)).toISOString()
// if (!dateTimesConst.includes(previousMonth)) prevLength = 0
const length = dateValuesConst[i].length
dateValues.push([prevLength, length])
dateTimes.push(dateTimesConst[i])
prevLength = length
if (length > mostBadgesPerMonth) mostBadgesPerMonth = length
// if (!dateTimesConst.includes(nextMonth) && i !== dateValuesConst.length - 1) {
// dateTimes.push(nextMonth)
// dateValues.push([length, 0])
// prevLength = 0
// }
}
function barColorCode() {
return (ctx) => {
const start = ctx.parsed._custom.start
const end = ctx.parsed._custom.end
return start <= end ? "rgba(50, 168, 82, 1)" : (start > end) ? "rgba(191, 27, 27, 1)" : "black"
}
}
const config = {
type: "bar",
data: {
labels: dateTimes,
datasets: [{
label: "Badges",
data: dateValues,
elements: {
bar: {
backgroundColor: barColorCode()
}
},
barPercentage: 1,
categoryPercentage: 0.95,
borderSkipped: false
}]
},
options: {
plugins: {
legend: {
display: false
},
title: {
display: true,
text: "Test",
color: "#FFFFFF"
}
},
scales: {
x: {
type: 'time',
title: {
display: true,
text: 'Date',
color: "#FFFFFF"
},
time: {
unit: "month",
round: "month"
},
min: dateTimesConst[0],
max: dateTimesConst[dateTimesConst.length - 1],
grid: {
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
ticks: {
color: "#FFFFFF"
}
},
y: {
title: {
display: true,
text: 'Number of Badges',
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
min: 0,
max: mostBadgesPerMonth + 1,
grid: {
borderColor: "#FFFFFF",
color: "#FFFFFF"
},
ticks: {
color: "#FFFFFF"
}
}
}
},
plugins: [
{
id: 'custom_canvas_background_color',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = '#303030';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
}
]
};
const imageBuffer = await canvasRenderService.renderToBuffer(config)
fs.writeFileSync("./chart2.png", imageBuffer)
Again, big thanks to #uminder for the inspiration.

Chartjs - make line start at ... but maintain x-axis data

I created a dynamic line chart based on some input data. The intention is that the customer can indicate with a dropdown on which month the "Investment" should start.
So, for example, if the "Investment" does not start until month 6, then that line should only start at 6 on the x-axis. But the other lines "Case" and "ROI" should still just start at 1.
I've tried several things but to no avail.
I tried changing the x-axis "min ticks" based on the selection the user made, but that makes all lines start at another point instead of the "Investment" line only. Another problem is that every number before the selection then dissapears from the x-axis. But I really want to keep every number from 1-60, even if the user chooses to start the "Investment" on month 10, for example.
I would really appreciate some help! Thanks.
Here's my fiddle: https://jsfiddle.net/js5pha24/
var options = {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Case',
data: [],
backgroundColor: 'rgba(152,164,135, 0.5)',
borderColor: 'rgb(152,164,135)',
fill: false
}, {
label: 'Case',
data: [],
backgroundColor: 'rgba(145,139,167, 0.5)',
borderColor: 'rgb(145,139,167)',
fill: false
}, {
label: 'Case',
data: [],
backgroundColor: 'rgba(206,157,206, 0.5)',
borderColor: 'rgb(206,157,206)',
fill: false
}]
},
options: {
legend: {
display: true,
position: "top"
},
scales: {
xAxes: [{
ticks: {
beginAtZero: true,
autoSkip: true,
maxRotation: 0,
minRotation: 0
}
}],
yAxes: [{
ticks: {
callback: value => {
return "€ " + value;
}
}
}]
}
}
}
for (let i = 1; i <= 60; i++) {
options.data.labels.push(i);
const caseMonth = 118187 * i;
options.data.datasets.find(set => set.label === "Case").data.push(caseMonth);
const investMonth = 500000 + (20000 * i);
options.data.datasets.find(set => set.label === "Investment").data.push(investMonth);
const roiMonth = caseMonth - investMonth;
options.data.datasets.find(set => set.label === "ROI").data.push(roiMonth);
}
var ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
canvas { background-color : #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.js"></script>
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
</body>
You can put null values on the chart data so one line can start after the others. For example if you want the investment line start at month 10, you can replace the the first ten investMonth values with null.
If understood correctly you still want to use the investMonth value in the roiMonth calculation so I created "investMonthValue" so only investment will get null if it is less than investmentStartMonth.
let investmentStartMonth = 10
for (let i = 1; i <= 60; i++) {
options.data.labels.push(i);
const caseMonth = 118187 * i;
options.data.datasets.find(set => set.label === "Case").data.push(caseMonth);
let investMonth = 500000 + (20000 * i);
let investMonthValue = i<investmentStartMonth?null:investMonth
options.data.datasets.find(set => set.label === "Investment").data.push(investMonthValue);
const roiMonth = caseMonth - investMonth;
options.data.datasets.find(set => set.label === "ROI").data.push(roiMonth);
}

Chart js animating a line while changing x-axis labels

I achieved animating a plot using Jukka Kurkela example here.
Now I am having trouble customizing this plot further.
Logic of the custom plot
The plot starts animating with the x-axis labels being 0-20. When the plot reaches 20 then update the x-axis to be 20-40. Increment i or 20 until the x-axis reach its limit.
How to apply the logic above to the Example below?
// Generating data
var data = [];
var prev = 100;
for (var i=0;i<200;i++) {
prev += 5 - Math.random()*10;
data.push({x: i, y: prev});
}
var delayBetweenPoints = 100;
var started = {};
var ctx2 = document.getElementById("chart2").getContext("2d");
var chart2 = new Chart(ctx2, {
type: "line",
data: {
datasets: [
{
backgroundColor: "transparent",
borderColor: "rgb(255, 99, 132)",
borderWidth: 1,
pointRadius: 0,
data: data,
fill: true,
animation: (context) => {
var delay = 0;
var index = context.dataIndex;
if (!started[index]) {
delay = index * delayBetweenPoints;
started[index] = true;
}
var {x,y} = index > 0 ? context.chart.getDatasetMeta(0).data[index-1].getProps(['x','y'],
true) : {x: 0, y: 100};
return {
x: {
easing: "linear",
duration: delayBetweenPoints,
from: x,
delay
},
y: {
easing: "linear",
duration: delayBetweenPoints * 500,
from: y,
delay
},
skip: {
type: 'boolean',
duration: delayBetweenPoints,
from: true,
to: false,
delay: delay
}
};
}
}
]
},
options: {
scales: {
x: {
type: 'linear'
}
}
}
});
<div class="chart">
<canvas id="chart2"></canvas>
</div>
<script src="https://www.chartjs.org/dist/master/Chart.js"></script>
Solved it! Instead of incrementing 20 seconds, it is incrementing every 5 seconds ahead of time. Definitely a better experience for the user.
Got help from Rowf Abd's post.
var myData = [];
var prev = 100;
for (var i=0;i<60;i++) {
prev += 5 - Math.random()*10;
myData.push({x: i, y: prev});
}
var ctx = document.getElementById('myChart').getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
data: [myData[0]],
pointRadius: 0,
fill: false,
borderColor: "black",
lineTension: 0
}]
},
options: {
legend: {
onClick: (e) => e.stopPropagation()
},
title:{
fontColor: 'Black'
},
layout: {
padding: {
right: 10
}
},
scales: {
xAxes: [{
type: 'linear',
ticks: {
}
}],
yAxes: [{
scaleLabel: {
// fontFamily: 'Lato',
fontSize: 19,
fontColor: "Black"
}
}]
}
}
});
var next = function() {
var data = chart.data.datasets[0].data;
var count = data.length;
var xabsmin = 20;
var xabsmax = 60;
var incVar = 5;
data[count] = data[count - 1];
chart.update({duration: 0});
data[count] = myData[count];
chart.update();
if (count < myData.length - 1) {
setTimeout(next, 500);
}
if (data[count].x < xabsmin) {
chart.config.options.scales.xAxes[0].ticks.min = xabsmin - xabsmin;
chart.config.options.scales.xAxes[0].ticks.max = xabsmin;
chart.update();
}
if(data[count].x >= xabsmin && data[count].x < (xabsmax)){
var currentT = parseFloat(data[count].x);
var modDiv = (currentT % incVar);
var tempXMax = (currentT) + (incVar - modDiv);
chart.config.options.scales.xAxes[0].ticks.max = tempXMax;
chart.config.options.scales.xAxes[0].ticks.min = tempXMax - xabsmin;
chart.update();
}
}
setTimeout(next, 500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>
<canvas id="myChart"></canvas>

Static response WORKS! While Asynchronous doesn't work

I'm creating an angular directive for realtime chart displaying here is the code which returns everything including link:function() { } inside directive.
Here is the code for static directive which works perfectly
angular.module('app').directive("flotChartRealtime", [
function() {
return {
restrict: "AE",
link: function(scope, ele) {
var realTimedata,
realTimedata2,
totalPoints,
getSeriesObj,
getRandomData,
getRandomData2,
updateInterval,
plot,
update;
return realTimedata = [],
realTimedata2 = [],
totalPoints = 100,
getSeriesObj = function() {
return [
{
data: getRandomData(),
lines: {
show: true,
lineWidth: 1,
fill: true,
fillColor: {
colors: [
{
opacity: 0
}, {
opacity: 1
}
]
},
steps: false
},
shadowSize: 0
}, {
data: getRandomData2(),
lines: {
lineWidth: 0,
fill: true,
fillColor: {
colors: [
{
opacity: .5
}, {
opacity: 1
}
]
},
steps: false
},
shadowSize: 0
}
];
},
getRandomData = function() {
if (realTimedata.length > 0)
realTimedata = realTimedata.slice(1);
// Do a random walk
//console.log(realTimedata);
while (realTimedata.length < totalPoints) {
var prev = realTimedata.length > 0 ? realTimedata[realTimedata.length - 1] : 50,
y = prev + Math.random() * 10 - 5;
if (y < 0) {
y = 0;
} else if (y > 100) {
y = 100;
}
realTimedata.push(y);
}
// Zip the generated y values with the x values
var res = [];
for (var i = 0; i < realTimedata.length; ++i) {
res.push([i, realTimedata[i]]);
}
return res;
},
getRandomData2 = function() {
if (realTimedata2.length > 0)
realTimedata2 = realTimedata2.slice(1);
// Do a random walk
while (realTimedata2.length < totalPoints) {
var prev = realTimedata2.length > 0 ? realTimedata[realTimedata2.length] : 50,
y = prev - 25;
if (y < 0) {
y = 0;
} else if (y > 100) {
y = 100;
}
realTimedata2.push(y);
}
var res = [];
for (var i = 0; i < realTimedata2.length; ++i) {
res.push([i, realTimedata2[i]]);
}
return res;
},
// Set up the control widget
updateInterval = 500,
plot = $.plot(ele[0], getSeriesObj(), {
yaxis: {
color: '#f3f3f3',
min: 0,
max: 100,
tickFormatter: function(val, axis) {
return "";
}
},
xaxis: {
color: '#f3f3f3',
min: 0,
max: 100,
tickFormatter: function(val, axis) {
return "";
}
},
grid: {
hoverable: true,
clickable: false,
borderWidth: 0,
aboveData: false
},
colors: ['#eee', scope.settings.color.themeprimary],
}),
update = function() {
plot.setData(getSeriesObj()); // getting .data filled here perfectly
plot.draw();
setTimeout(update, updateInterval);
},
update();
}
};
}
]);
My code with HTTP request which doesn't work
getSeriesObj = function () {
return [{
data: getRandomData(function(res) {
console.log(res) // getting array result here from http call but not returning to data:
return res;
}),
lines: {
show: true,
lineWidth: 1,
fill: true,
fillColor: {
colors: [{
opacity: 0
}, {
opacity: 1
}]
},
steps: false
},
shadowSize: 0
}, {
data: getRandomData2(function (res) {
return res;
}),
lines: {
lineWidth: 0,
fill: true,
fillColor: {
colors: [{
opacity: .5
}, {
opacity: 1
}]
},
steps: false
},
shadowSize: 0
}];
},
getRandomData = function (callback) {
var authToken = window.localStorage.getItem('token');
var url = $rootScope.apiPath + 'Elasticsearch/countget?token=' + authToken;
var res = [];
$http.get(url).then(function (result) {
realTimedata = result.data;
if (realTimedata.length > 0)
//result = [10,22,33,11,32,88,77,66,21,90,92,98,99.9,88.8,76,66,56,88];
for (var i = 0; i < realTimedata.length; ++i) {
var y = realTimedata[i] + Math.random() * 10 - 5;
if (y < 0) {
y = 0;
} else if (y > 100) {
y = 100;
}
res.push([i, y]);
}
callback(res);
});
},
Problem:
When i try following code:
update = function () {
//console.log(getSeriesObj());
plot.setData(getSeriesObj()); // .data property gets undefined
plot.draw();
setTimeout(update, updateInterval);
}
function getSeriesObj() return array of object which return data property to undefined what can be the reason?
how can i resolve this?
Note: This is far different from this question.
When do this
data: getRandomData(function(res) {
return res;
});
You assign the rValue of getRandomData to data.
As stated in your post, getRandomData now has no return statement, so return undefined.
The main problem here is that you expect that plot.setData(getSeriesObj()); work synchronously
Steps
get the data to fill plot
set the data to the plot
draw it
update the values again
Now as the http request work async you cannot expect to retrieve a value from getSeriesObj(). You have to think that getSeriesObj work now async so you can only work with callback that will be fired when the resource is ready to be used
so the update method become
update = function () {
var updateTime = +new Date;
getSeriesObj(function(res){ // execute that stuff when ready
plot.setData(res);
plot.draw();
setTimeout(update, Math.max(10, updateInterval - (+new Date - updateTime)) );
});
}
and getSeriesObj
getSeriesObj = function (callback) {
getRandomData(function(res) {
getRandomData2(function(res2){
var data = [{
data: res,
lines: {
show: true,
lineWidth: 1,
fill: true,
fillColor: {
colors: [{
opacity: 0
}, {
opacity: 1
}]
},
steps: false
},
shadowSize: 0
}, {
data: res2,
lines: {
lineWidth: 0,
fill: true,
fillColor: {
colors: [{
opacity: .5
}, {
opacity: 1
}]
},
steps: false
},
shadowSize: 0
}];
callback(data); // now the ressource obj is now ready to be used
});
});
}

Categories