Can't get my ChartJS line graph to redraw reactively


#1

Hi,

I have had a good hard search around and have managed to create a chart using chartjs and looking up the data from my mongo collection. But it won’t update when I add another item to that collection. The examples I have seen all seem to do this differently and none of them are working for me. I have thrown the same question up on reddit but the community seems a bit more active here so hoping for a response.

I think the issue is in the Tracker.autorun() it doesn’t fire after I add more data.

Code below

Publish

if (Meteor.isServer) {
    Meteor.publish("alerts", function alertsPublication() {
        return Alerts.find();
    });
}

Template
<template name = "dashboardChart" >
    <canvas id = "dashboardChart" height = "125" >  </canvas >
</template >

JavaScript

import {Template} from 'meteor/templating';
import {Chart} from 'chart.js/src/chart.js';
import {Alerts} from '../../api/alerts.js';

import './dashboardChart.html';

Template.dashboardChart.onCreated(function () {

});

Template.dashboardChart.onRendered(function () {
    const alerts = this.subscribe("alerts");

    Tracker.autorun(function () {
        if (alerts.ready()) {
            const datalabels = [];
            const datavalues = [];

            const now = new Date();

            const lookupStartDate = new Date();
            lookupStartDate.setMonth(lookupStartDate.getMonth() - 1);
            lookupStartDate.setHours(0, 0, 0, 0);

            const lookupEndDate = new Date();
            lookupEndDate.setMonth(lookupEndDate.getMonth() - 1);
            lookupEndDate.setHours(24, 0, 0, 0);

            while (lookupStartDate < now) {
                datalabels.push(moment(lookupStartDate).format('ddd Do MMM'));

                datavalues.push(
                    Alerts.find({
                        createdAt: {
                            $gte: lookupStartDate,
                            $lte: lookupEndDate,
                        },
                    }).count()
                );

                lookupStartDate.setDate(lookupStartDate.getDate() + 1);
                lookupEndDate.setDate(lookupEndDate.getDate() + 1);
            }

            const ctx = document.getElementById("dashboardChart");
            const myChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: datalabels,
                    datasets: [{
                        label: 'Alerts',
                        data: datavalues,
                        fill: false,
                        borderColor: 'rgba(255,99,132,1)',
                        backgroundColor: 'rgba(255,99,132,1)',
                        borderWidth: 1
                    }]
                },
                options: {
                    legend: {
                        display: false
                    },
                    responsive: true,
                    tooltips: {
                        mode: 'index',
                        intersect: false,
                    },
                    hover: {
                        mode: 'nearest',
                        intersect: true
                    },
                    scales: {
                        xAxes: [{
                            display: true,
                            scaleLabel: {
                                display: false,
                                labelString: 'Date'
                            }
                        }],
                        yAxes: [{
                            display: true,
                            ticks: {
                                beginAtZero: true
                            },
                            scaleLabel: {
                                display: true,
                                labelString: 'Number of Alerts'
                            }
                        }]
                    }
                }
            });
        }
    });

});

#2

I can’t test it now but I am not sure that count() is reactive.

You can try:

datavalues.push(
  Alerts.find({
     createdAt: {
          $gte: lookupStartDate,
          $lte: lookupEndDate,
      },
  }).fetch().length
);

But here it would be best to create a custom publication server side to publish the count. There is an exemple doing exactly this in the API doc: https://docs.meteor.com/api/pubsub.html

Also you shoud use this.autorun instead of Tracker.autorun so the computation is automatically stopped when the template is destroyed.

EDIT : you also should write the graph rendering code (new Chart…) outside of the autorun or you will create a new object everytime the function is run. On rendered, create an empty chart. Then when subscription is ready, update the chart with myChart.update() in the autorun.


#3

I use HighCharts, but the principles should be the same for ChartJS.

I don’t see anything immediately wrong with the structure. However, you don’t destroy the old chart, which means you are at risk of a memory leak. Also, you don’t save the tracker computation object for stop()ing it on template tear-down, so you’re also at risk of multiple instances of Tracker running.

Other than those observations, my own (HighCharts) code looks like:

Template.chart.onRendered(function chartOnRendered() {
  this.data = [];
  this.subscribe('things', () => { // removes the need for an onReady() test
    // code to populate this.data array (find().fetch()) and render initial chart
    this.autorun(() => { // template autoruns will be cleaned up for me
      // code to update this.data (find().fetch()) from the collection and re-draw series (this is a lightweight operation).
    });
  });
});

#4

Thanks for the tips guys. I shoved all the code inside the autorun in a last ditch attempt to get it to work. Thinking maybe something was out of scope.

I will try out your suggestions tonight and get back to you. I will also try and investigate if count is reactive.

I was trying to avoid high charts as there is a possibility this will be used in a commercial application and I don’t need the headache of license costs.


#5

I just built an entire app based on ChartJS with Blaze, so don’t worry it will work. I display a real time chart fetching millions of values and it works pretty well.

For your information, I used methods instead of publications to get chart data. I am working on datasets with millions of points, so publication wasn’t following. What I did was just gettting initial values from a method, the method returns data and the date of the last point. When I get the result I update the chart, then I recall the method using the date I received to get just new values, and I update the chart, and so on. It works very well if you need a real time chart.


#6

count() is reactive because the find() is reactive.


#7

You should be able to adopt the same principles with ChartJS.


#8

I wasn’t sure because of this in the doc:

Cursors are a reactive data source. On the client, the first time you retrieve a cursor’s documents with fetch, map, or forEach inside a reactive computation (eg, a template or autorun), Meteor will register a dependency on the underlying data.

I understand only fetch, map and forEach make the autorun reactive.


#9

fetch, map and forEach don’t make the autorun reactive. Basically, if find discovers it’s being used inside an autorun, it essentially invalidates the computation when the cursor changes, which makes the autorun re-run. If there’s a cursor method which follows on, that will be executed.

const num = Collection.find().count();
// is the same as
const cursor = Collection.find();
const num = cursor.count();

#10

I just test it creating a fast Meteor app.

So yes, count() is reactive. But if I just get the cursor without calling fetch or count, autorun doesn’t run again.

This will be called everytime database is updated:

this.autorun(() => {
  console.log('autorun');
  const todos = Todos.find().count();
});

This is not going to be rerun:

this.autorun(() => {
  console.log('autorun');
  const todos = Todos.find();
});

Coming back to the initial issue, are you sure the autorun is not recall? Have you tried to put a console.log() at the beginning of the autorun? Maybe this is just your while condition that is false. What if you attach lookupStartDate to the template instance: this.lookupStartDate instead of const lookupStartDate?


#11

Yes I threw some console logging in when I was trying to debug. So on a page refresh autorun stopped as alerts was not ready. It then immediately ran again and populated the graph with data. I verified this by stepping through using the chrome debugger.

I then inserted more data but autorun did not rerun. I also have a counter that is updated using a helper on the same page and it was updated.


#12

This could be because lookupStartDate is not attach to the template. Try to declare and use it with this.lookupStartDate instead of const lookupStartDate.

If it is still not working, you can also try to remove $lte: lookupEndDate from the mongo request and see if it rerun. I think the issue is because your mongo request parameters are not updated, so the result is always the same and autorun is not recalled.


#13

So I have followed Robs advice and restructured the code to match his template but no luck.

I have changed nothing on the template or the publish but here is the client side code

import { Template } from 'meteor/templating';
import { Chart } from 'chart.js/src/chart.js';
import { Alerts } from '../../api/alerts.js';
import './dashboardChart.html';

function buildDataSeries(datalabels, datavalues) {
  console.log('inside buildDataSeries');
  const now = new Date();

  const lookupStartDate = new Date();
  lookupStartDate.setMonth(lookupStartDate.getMonth() - 1);
  lookupStartDate.setHours(0, 0, 0, 0);

  const lookupEndDate = new Date();
  lookupEndDate.setMonth(lookupEndDate.getMonth() - 1);
  lookupEndDate.setHours(24, 0, 0, 0);
  
  while (lookupStartDate < now) {
    datalabels.push(moment(lookupStartDate).format('ddd Do MMM'));

    datavalues.push(
      Alerts.find({
        createdAt: {
          $gte: lookupStartDate,
          $lte: lookupEndDate,
        },
      }).fetch().length
    );

    lookupStartDate.setDate(lookupStartDate.getDate() + 1);
    lookupEndDate.setDate(lookupEndDate.getDate() + 1);
  }
}

Template.dashboardChart.onRendered(function chartOnRendered() {
  console.log('inside onRendered');
  this.datalabels = [];
  this.datavalues = [];

  const ctx = document.getElementById("dashboardChart");
  const myChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels: this.datalabels,
      datasets: [{
        label: 'Alerts',
        data: this.datavalues,
        fill: false,
        borderColor: 'rgba(255,99,132,1)',
        backgroundColor: 'rgba(255,99,132,1)',
        borderWidth: 1
      }]
    },
    options: {
      legend: {
        display: false
      },
      responsive: true,
      tooltips: {
        mode: 'index',
        intersect: false,
      },
      hover: {
        mode: 'nearest',
        intersect: true
      },
      scales: {
        xAxes: [{
          display: true,
          scaleLabel: {
            display: false,
            labelString: 'Date'
          }
        }],
        yAxes: [{
          display: true,
          ticks: {
            beginAtZero: true
          },
          scaleLabel: {
            display: true,
            labelString: 'Number of Alerts'
          }
        }]
      }
    }
  });


  this.subscribe('alerts', () => { // removes the need for an onReady() test
    // code to populate this.data array (find().fetch()) and render initial chart
    console.log('inside subscribe');
    buildDataSeries(this.datalabels, this.datavalues);

    myChart.update();

    this.autorun(() => {
      // template autoruns will be cleaned up for me
      // code to update this.data (find().fetch()) from the collection and re-draw series (this is a lightweight operation).
      console.log('inside AutoRun');

      while (this.datalabels.length > 0) {
        this.datalabels.pop();
      }

      while (this.datavalues.length > 0) {
        this.datavalues.pop();
      }

      buildDataSeries(this.datalabels, this.datavalues);

      myChart.update();
    });
  });
});

And here is the output after refreshing the page on the console

The `stylus` package has been deprecated.

To continue using the last supported version
of this package, pin your package version to
2.513.14 (`meteor add stylus@=2.513.14`).
(anonymous) @ stylus.js?hash=09561c77c28e9e1a9ab7476a5c681bc0406cf0dc:26
(anonymous) @ stylus.js?hash=09561c77c28e9e1a9ab7476a5c681bc0406cf0dc:36
(anonymous) @ stylus.js?hash=09561c77c28e9e1a9ab7476a5c681bc0406cf0dc:42
dashboardChart.js:40 inside onRendered
DevTools failed to parse SourceMap: http://localhost:3000/bootstrap.css.map
dashboardChart.js:96 inside subscribe
dashboardChart.js:8 inside buildDataSeries
dashboardChart.js:104 inside AutoRun
dashboardChart.js:8 inside buildDataSeries

I then inserted another alert item in the collection but nothing came up on the console and debugger showed autorun had not fired.


#14

I have it working after much more digging around I have added this line of code to the autorun

var myCursor = Alerts.find().fetch();

So now I have a reactive variable in the autorun. where as previously I just had a none reactive array of numbers and dates.