Simple real-time graphs with Morris.js and jQuery
During a recent hardware hackathon, held by PCH International on DCU’s Innovation Campus, the team I was a part of created a fridge monitoring setup for pharmacies.
We built a “smart-shelf” – pressure-sensitive shelving prototyped in acrylic using a laser cutter and hooked up to an Intel Galileo – and combined it with an EpiSensor temperature monitor. These components fed their data up to a Node-RED instance running on IBM’s Bluemix platform, which in turn piped it through to a couple of MongoLab-hosted collections.
I was mainly focused on building the web side, using my hackathon-starter-handlebars Node.js project as the base (itself a port of hackathon-starter which uses Jade out of the box).
On this platform I built us a nice web dashboard to present the data. One of the aces up my sleeve was a pretty, real-time graph powered by Morris.js and jQuery AJAX calls to a Node.js API.
The emphasis was on speed and simplicity, and I feel this could totally be useful for other people out there who need to inject some dynamism into their graphing. I’ll show you how I did it.
Powering this example is a simple (and extremely limited) Node.js API powered by Express.
Imagine we have a MonogDB collection filled with entries like the following:
{ "_id": "507f1f77bcf86cd799439011", "deviceId": 1, "time": "2014-09-24T11:01:43.511Z", "temp": 2.1 }
And can grab this data from MongoDB in a Node.js function like so:
/** * (from a Node.js controller file in controllers/device.js) * GET /devices/:id/recent-temps * Device page. * API Only */ exports.getRecentDeviceTemps = function(req, res) { // Make the call to MongoDB to grab the relevant records req.db.tempReadings.find({ deviceId: req.params.id }, null, { limit: 120, sort: { 'time': -1 } }).toArray(function(err, results) { if (err) { next(err); } res.format({ // Respond to normal browser requests with the 404 page html: function() { res.render('404', { title: 'Page not found' }); }, // Respond to AJAX requests with Morris-consumable data json: function() { // Initialise an array for returning later var graphData = []; // Go through each result for (var i = 0; i < results.length; i++) { var result = results[i]; // Check these results are valid if (result.times && result.temp) { var date = new Date(result.time); var temp = result.temp; // Create an object for Morris.js to read var graphPoint = {}; graphPoint.timestamp = date.getTime(); graphPoint.temp = temp; // Push the object to the array for returning graphData.push(graphPoint); } } // Return the graphData object as JSON res.json({ graphData: graphData }); } }); }); };
The :id in the comments is a nice Express paradigm enabling us to write URLs REST-style, like example.com/users/1 – in this case we could process this in Node by writing the route /users/:id. Here the :id parameter refers to the device ID, and is checked by reading req.params.id (:id -> req.params.id). Gotta love them RESTful APIs!
The important part to look at begins at the json: function() property of the res.format object. The presence of this property tells Express to use the json function in response to an AJAX (JSON) request to the URL. For HTML requests to that URL we are giving back a 404 as this isn’t applicable to our purposes.
When we receive the data from the MongoDB call (results), we loop through each result (for our purposes a list of datetime and corresponding temperature value as exampled above), taking the date and time and placing them in a graphPoint object.
Once I have the necessary data for the result, I push it onto an array of graphPoints called graphData, which is returned to the AJAX request by the res.json() call once the looping is done and all the results have been processed. This gives us a nice array of JSON objects that Morris will be able to read with only a little bit of prompting on our parts.
It’s especially important to note that this kind of parsing could be done on the frontend side if you are not the creator of the API. You just need to make your API call and then construct your elements as you need them. You may not even need to do any parsing. Technically I don’t really need to here (as the original MongoDB records have the information in a readily accessible state that could be pointed to for Morris), but am doing so for demonstration purposes.
Let’s take a look at where this data is requested, and how the Morris graph is initialised.
/* * From a frontend js file loaded in-browser, something like a main.js */ function renderLiveTempGraph() { // Get ready to store our graph instance in a variable var mainGraph; // Call our API $.getJSON('/devices/1/recent-temps', function(results) { // Initialise a Morris line graph and store it in mainGraph mainGraph = Morris.Line({ element: 'main-graph', // Tell Morris where the data is data: results.graphData, // Tell Morris which property of the data is to be mapped to which axis xkey: 'timestamp', ykeys: ['temp'], postUnits: ' °c', lineColors: ['#199cef'], goals: [6.0], goalLineColors: ['#FF0000'], labels: ['Temperature'], lineWidth: 3, pointSize: 2, resize: true }); // Set up an interval on which the graph data is to be updated // Note the passing of the mainGraph parameter setInterval(function() { updateLiveTempGraph(mainGraph); }, 20000); }); }
As you can see here, a simple jQuery.getJSON() function is used to request the necessary data from the (completely unsecured, don’t-do-this-for-commercial-projects API). Once the data is received, we remove the loading gif we had in place of the graph and initialise a new Morris.js line graph.
Note that we tell Morris what the important data object is – results.graphData – and that what we named the properties of the graphPoint objects in graphData is important because we explicitly tell Morris how to map graphData’s properties to the axes. In this case we want to put timestamp on the x and temp on the y, giving us a nice temperature over time graph.
The really interesting part is that we initialise the line graph and assign it to a variable – mainGraph. Once the graph is rendered the first time, we setup a normal Javascript setInterval call to an updateLiveTempGraph() function, passing the mainGraph object and repeating it every twenty seconds. This interval could be whatever you want. In our case we were only receiving temperature data every twenty seconds.
This is the meat of the real-time updating, so what’s happening in that update call?
/* * From the same frontend js file */ function updateLiveTempGraph(mainGraph) { // Make our API call again, requesting fresh data $.getJSON('/devices/1/recent-temps', function(results) { // Set the already-initialised graph to use this new data mainGraph.setData(results.graphData); }); }
Simply put, we are requesting the new data from the API and telling the previously initialised Morris line graph (mainGraph) to set its data to the new payload.
Morris is so smart that it doesn’t need to be told a second time how to parse this data (where to look for the data and which axes to map to what) – we simply pass the new data object (the same object we told Morris to look at in the initialisation of the graph) and it does the rest, reading the timestamp and temp and updating our graph right before our eyes.
That’s really all there is to it. Nothing fancy, but if you need real-time (or as near to it as you could want) graphing, without complicated coding, give this a shot.