[SOLVED] Reactive json data object for High Charts


#1

I’ve got a collection where I’m storing ping time data and the date / time it was run.

I’m using High Charts from the maazalik:highcharts package.

I can get the chart to populate, but I pass a variable to my publish function to limit the publish to 100 of the last pings for the URL the user clicks on.

So something like

Meteor.publish("pingStatus", function(myUrl) {
    return PingStatus.find({ url: myUrl }, { sort:  { runOn: -1 }, limit: 100 }});
});

When I click a row on a table, I pass the URL of the row clicked, to get the last 100 ping times for that url. I then open a modal with the high chart on it, and display a line graph of the ping time data.

It works, but I’ve noticed sometimes the data is not being refreshed after the publish function updates the data. I’ve tried using a timeout to give it time to get the new data, but it just isn’t working.

Is there a way to get the data in the highcharts object to use my reactive source? I’m using a Session.set to create the object, then Session.get to get it and use it in the high charts object, but still I’m seeing it not update the data.

I can post more code if needed.


#2

That’s using a very old version of HighCharts. The official

npm version is up to date and just as easy (maybe easier) to use.

There’s an error in that. It should be return PingStatus.find({ url: myUrl }, { sort: { runOn: -1 }, limit: 100 });

Assuming you’re using Blaze, the code for this on the client would look something like:

Template.chart.onCreated(function chartOnCreated() {
  this.subscribe('pingStatus', myUrl);
});

Template.chart.onRendered(function chartOnRendered() {
  this.data = [];
  this.chart = codeToRenderChart // Render (empty) chart here using this.data as the array of points
  this.autorun(() => {
    if (this.subscriptionsReady()) { // Whenever the data changes...
      this.data = PingStatus.find({ url: myUrl }, { sort: { runOn: -1 }, limit: 100 })
      .map(doc => {
        // code to make the data array match Highchart expectations
      });
      this.chart.series[0].setData(this.data);
    }
  });
});

#3

Thanks @robfallows. On the last part of the code you show in the onRendered function, where you query the PingStatus collection, do you have to again request my url, and sort and filter, if that’s already being done in the publish function?

I’m pretty sure the answer is ‘No’, but want to make sure. I’ve noticed when I limit through my publication, then I can generally just query like

PingStatus.find({});

as this will only bring back what I am publishing anyway. I do know I can further filter down from there.


#4

It’s best practice to use the exact same query on the client as on the server for the following reasons:

  • sort: The order of documents on the client may not match those on the server.
  • limit: New documents coming in (for example new data points) will cause a brief overlap if the limit is exceeded.
  • url: Changing the subscription (for example a different url) will cause a brief overlap of old and new documents.

#5

Thanks Rob, didn’t realize that about the overlap (or I guess I did, but didn’t think about it causing a delay).

I’ve setup the HighCharts like you’ve shown, but I get an error on this line

this.chart.series[0].setData(this.data);

of

Cannot read property '0' of undefined.

I then changed it to remove the [0] and get

Cannot read property setData of undefined.

Here’s the code I have in the onRendered function.

let myUrl = Session.get("myUrl");

    let myObj = Session.get("myObj");
    console.dir(myObj);
    this.data = [];
    HighCharts.chart('container', {
            chart: {
                plotBackgroundColor: null,
                plotBorderWidth: null,
                plotShadow: false
            },
            title: {
                text: "Ping Times"
            },
            tooltip: {
                pointFormat: '<b>{point.y} ms</b>'
            },
            plotOptions: {
                line: {
                    allowPointSelect: true,
                    cursor: 'pointer',
                    dataLabels: {
                        enabled: true,
                        format: null,
                        style: {
                            color: (HighCharts.theme && HighCharts.theme.contrastTextColor) || 'red'
                        },
                    }
                }
            },
            series: [{
                type: 'line',
                name: 'Days / Ping Times',
                data: myObj
            }]
        });

        this.autorun(() => {
            if (this.subscriptionsReady()) { // Whenever the data changes...
                this.data = PingStatus.find({ url: myUrl }, {sort: { runOn: -1 }, limit: 100 })
                .map(doc => {
                    return [doc.runOn, doc.pingTime]
                });
                this.chart.series.setData(this.data);
            }
        });

If I change the last part from this.chart.series.setData(this.data) to Session.set("myObj", this.data) then I get points on my chart, but again they don’t update to show the right points based on the myUrl value I pass to the query.


#6

Thought I had it - but didn’t…so editing this comment out.


#7

Ok, I was wrong.

Still not getting the updated values. It’s one click behind. The Object is for some-reason getting the last set of values.

I’m open to suggestions.

I’ll post my code later so you can take a look, have a good chuckle, scratch your heads, and wonder how I get anything to work. Then perhaps offer some suggestions.

Best,


#8

Ok, I’ve pushed my code up to https://github.com/bmcgonag/hostUp.git

The files of interest are in

  • client/hostList/hostList.html
  • client/hostList/hostList.js
  • client/pingList/pingListhtml <== this is the template that is shown in a modal from the hostList template.
  • client/pingList/pingList.js

#9

Made an adjustment this morning.

Broke out the chart into it’s own template (more in line with what @robfallows shows above.

<template name="chart">
    <div id="container"></div>
</template>

and have this in the js for that template

import { PingStatus } from '../../imports/api/pingStatus.js';
import HighCharts from 'highcharts';

Template.chart.onCreated(function() {
        let myUrl = Session.get("myUrl");
        this.subscribe("pingStatuses", myUrl);
});

Template.chart.onRendered(function chartOnRendered() {
    let myUrl = Session.get("myUrl");
    this.data = [];
        
    this.chart = {
        chart: {
            plotBackgroundColor: null,
            plotBorderWidth: null,
            plotShadow: false
        },
        title: {
            text: "Ping Times"
        },
        tooltip: {
            pointFormat: '<b>{point.y} ms</b>'
        },
        plotOptions: {
            line: {
                allowPointSelect: true,
                cursor: 'pointer',
                dataLabels: {
                    enabled: true,
                    format: null,
                    style: {
                        color: (HighCharts.theme && HighCharts.theme.contrastTextColor) || 'red'
                    },
                }
            }
        },
        series: [{
            type: 'line',
            name: 'Days / Ping Times',
        }]
    }

    this.autorun(() => {
        if (this.subscriptionsReady()) { // Whenever the data changes...
            this.data = PingStatus.find({ url: myUrl }, { sort: { runOn: -1 }, limit: 100 })
            .map(doc => {
                return [doc.runOn, doc.pingTime]
            });
            this.chart.series[0].setData(this.data);
            // var myObj = this.data
        }
        
    });
});

When I run it and click on a URL I get an error:

Type Error: _this.chart.series[0].setData is not a function

Still not sure where I’m going awry here.


#10

I haven’t had much time to look at this today. However, I’ve done a quick pass through pingResult.js and got this: untested.

import { PingStatus } from '../../imports/api/pingStatus.js';
import HighCharts from 'highcharts';

Template.pingResult.onCreated(function () {
  this.autorun(() => {
    this.subscribe("pingStatuses", Session.get("myUrl"));
  });
});

Template.pingResult.onRendered(function () {
  $('.modal').modal();

  this.autorun(() => {
    let myUrl = Session.get("myUrl"); // will redraw the base chart if the url changes (if you don't want to do this, move this line above the autorun)
    this.chart = HighCharts.chart('container', { // draw the base chart
      chart: {
        plotBackgroundColor: null,
        plotBorderWidth: null,
        plotShadow: false
      },
      title: {
        text: "Ping Times"
      },
      tooltip: {
        pointFormat: '<b>{point.y} ms</b>'
      },
      plotOptions: {
        line: {
          allowPointSelect: true,
          cursor: 'pointer',
          dataLabels: {
            enabled: true,
            format: null,
            style: {
              color: (HighCharts.theme && HighCharts.theme.contrastTextColor) || 'red'
            },
          }
        }
      },
      series: [{
        type: 'line',
        name: 'Days / Ping Times',
        data: this.data // the data will eventually appear here
      }]
    });

    if (this.subscriptionsReady()) { // Whenever the data changes or is first available...
      this.data = PingStatus.find({}) // (re)generate the series data
        .map(doc => {
          return [doc.runOn, doc.pingTime]
        });
      this.chart.series[0].setData(this.data); // and give it to be re-rendered
    }
  });
});

Template.pingResult.helpers({
  myUrl: function () {
    return Session.get("myUrl");
  },
});

Template.pingResult.events({
  'click .pingTimeModal'(event) {
    event.preventDefault();
    Session.set("myUrl", "");

    Session.set("pingVis", false);

    let thisModal = document.getElementById("modal-ping");
    thisModal.style.display = "none";
  }
});

#11

Thanks Rob. I know it’s painful to go through someone else’s code. I saw a couple of things I was not doing right.

Changed this.chart.ser... to HighCharts.chart.ser... and that helped get rid of my ..is not a function error on that line.

But now I"m back to the ‘cannot read property 0 of undefined’.

I’m sure this is also something simple…so I’m digging in a bit.


#12

I just pasted that code block into a quick mini-app and it works as I would expect - draws the chart, populates the line, then if I insert a new document into mongo shell it updates the line immediately.


#13

I obviously had a type or something wrong somewhere. I’ll use Git to compare what I got from you, to what I had, and see what was different.

Thanks so much for this. I’m learning slowly, but definitely not a dev’s dev. @robfallows rocks.