Dynamic Sparkline Example

We can adapt our simple static sparklines example to be updated dynamically. We'll use a simple server program written in Ruby, using Ruby's neat built-in WEBrick server (see www.webrick.org)

The demo should work on UNIX systems (you'll need ruby installed). To try the demo out, download sparklines.zip and unzip it. Run the web server using the command:

ruby sparklne.rb

Point your browser at http://localhost:8000/index.html. You should see (after a delay of 5 seconds) the sparkline appear and start updating.

The ruby script serves files as usual, but also creates a servlet to listen for requests on the /uptime URL and return data collected from the server.

sparkline.rb
# ruby web server including uptime servlet
# based on example from www.webrick.org

require 'webrick'
require 'net/http'

include WEBrick


$port = "8000"
$hostname = `hostname`.chomp()

# $fwds = [["localhost",8001],["localhost",8002]]
$fwds = []

# parse args
if ARGV.length>0
    $port = ARGV[0]
    if ARGV.length>1
        $hostname = ARGV[1]
    end
end

# call system uptime and parse result to return an uptime response as a JSON string
# format is "[<hostname>,<uptime-1min>,<uptime-5min>,<uptime-15min>]"
$matchstr = "load average:"
def uptime()
    uptext = `uptime`.chomp()
    idx = uptext.index($matchstr)
    if idx != nil
        uptext.slice!(0,idx+$matchstr.length).strip()    
    end
    return '["'+$hostname+'",'+uptext+"]"
end
    
# servlet for collecting and returning a JSON string with a list of 1 or more uptime responses
class UptimeServlet < WEBrick::HTTPServlet::AbstractServlet

  def do_GET(request, response)
    if $fwds.length > 0
        $fwds.each do |fwd|        
            if response.body != ''
                response.body += ","
            end    
            response.body += Net::HTTP.get_response(fwd[0], '/uptime', fwd[1]).body
        end
    else
        response['Content-Type'] = 'text/plain'
        response.status = 200
        response.body = uptime()
    end
  end
end

# ----------------------------------------------
# Create an HTTP server
s = HTTPServer.new(
  :Port            => $port,
  :DocumentRoot    => "."
)

s.mount("/uptime", UptimeServlet)

# When the server gets a control-C, kill it
trap("INT"){ s.shutdown }

# Start the server
s.start

The server's first task is to serve the HTML file index.html.

index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
    <head>
        <title>Sparkline Demo</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta http-equiv="Cache-Control" content="no-cache" />
        <meta http-equiv="Expires" content="0" />
        <meta name="author" content="Niall McCarroll" />
        <meta name="keywords" content="" />
        <meta name="description" content="" />
        <link rel="stylesheet" href="sparkline.css" type="text/css"  />
	    <script src="xhr.js" type="text/javascript"></script>
	    <script src="tinplate.js" type="text/javascript"></script>
	    <script src="sparkline.js" type="text/javascript"></script>
    </head>
    <body onload="startsparks();">
	<div id="uptime"></div>
    </body>
</html>

The html file uses the javascript code below to poll the web server at regular intervals via AJAX requests to the server's uptime servlet, and process the JSON message response detailing the server's load average times.

sparkline.js
/* sparkline.js - fetch data from server and format into a sparkline using tinplate
   Copyright (c) 2008 Niall McCarroll  
   Distributed under the MIT/X11 License (http://www.mccarroll.net/snippets/license.txt)
*/

hosts = {}; // mapping from hostname to a list of Number objects describing uptime (most recent first)

// tinplate template for the table holding all sparklines
// each row contains hostname|image with scale for sparkline|sparkline|most recent reading as a number
var tableformat = [ "<table>", 
    { foreach: [ 
        "<tr>", 
        '<td>$</td><td><img src="images/scale.png"></img></td><td id="sparkline_$"></td><td id="latest_$"></td>', 
        "</tr>" ] }, 
    "</table>" ];

// tinplate template for a sparkline graphic
var sparkformat = [ 
    '<div class="sparkline">&nbsp;', 
    { foreach: [ '<div class="spark$(colour)" style="top:$(y)px; left:$(x)px;>"></div>' ] }, 
    '</div>' ];

var sparkheight = 32;   /* height of each sparkline (pixels) must match sparkline.css */
var sparkwidth = 90;    /* width of each sparkline (pixels) must match sparkline.css */
var sparkpoint = 3;     /* point size (pixels), must match sparkline.css */
var timekeep = sparkwidth / sparkpoint; /* max number of times to keep and display in a sparkline */
var update_interval = 5000; /* time between updates, in milliseconds */

var timer = null;

// start the regular updating of sparklines
function startsparks() {
    timer = setInterval("updatesparks()",update_interval);
}

// fetch data from the server and add/update sparklines on this page
function updatesparks() {
    // fetch uptime information as JSON string and evaluate it
	var txt = "[" + xhr_fetch("/uptime") + "];";
	var hostlist = eval(txt);

	var h;
    // insert unknown sample (-1) for all known hosts and trim times
	for(h in hosts) {
		hosts[h].splice(0,0,-1);
        while(hosts[h].length > timekeep) {
            hosts[h].pop();
        }
	}

    // check to see if incoming information contains new hosts, will need to
    // update table to add new entries if so
    var update_table = false;
	for(h in hostlist) {
		var hostinfo = hostlist[h];
		var hostname = hostinfo[0];
		if (!(hostname in hosts)) {
            // new host
			hosts[hostname] = [-1];
			update_table = true;
		} 
        hosts[hostname][0] = Number(hostinfo[1]);
	}

	if (update_table) {
        // refresh table, new hosts have been detected 
		var allhosts = [];
		for(h in hosts) {
			allhosts.push(h);
		}
		var t = new tinplate();
		document.getElementById("uptime").innerHTML = t.process(tableformat,[allhosts]);
	}
	
    // generate the updated sparklines for each host
    for (h in hosts) {
        var points = [];
        var t;
        for(x in hosts[h]) {
            var tm = hosts[h][x];
            if (tm < 0) {
                // skip - no valid reading - do not plot a point 
                continue;
            }
            var y;
            if (tm <= 1.0) {
                // for uptime bewteen 0 and 1 use a linear scale
                y = Math.round(tm * sparkheight/2);         
            } else {
                // for uptime above 1 use a logarithmic scale
                y = Math.round(sparkheight/2 + (Math.log(tm)*sparkheight/2));
            }
            y = (y > sparkheight) ? 0 : (sparkheight - y);
            var colour = (tm <= 1.0) ? 'green' : 'red';
            points.push( { 'x':x*sparkpoint, 'y':y, 'colour':colour } );
        }
        

        // update the sparkline 
        t = new tinplate();      
        document.getElementById("sparkline_"+h).innerHTML = t.process(sparkformat,[points]);
 
        // update the latest reading (number)
        document.getElementById("latest_"+h).innerHTML = (hosts[h][0] > -1) ? String(hosts[h][0]) : ""; 
   }
}

You can customise sparkline.rb to forward requests to other instances of the script running on other hosts, and collate their reponses. The comments in this script should hopefully show you how. The javascript code in sparkline.js will render sparklines for each host.


 

Leave a comment

Anti-Spam Check
Comment