Working with the chart.js Javascript Package

Internet Outage Issues

Around June of 2018 I was becoming more and more upset with our cable modem internet connection.  The modem was probably 8 years old and was disconnecting quite regularly.  I started looking for a way to log the outages so I could have some evidence when complaining to our ISP.  The modem log would be a good start, but that doesn’t really tell me if I can really get out to things like google.com.  So I thought I’d use another method.

On my home centos Linux server, I wrote this quick php script and started it up at each reboot with an rc.d entry:

<?
//================================================================
// ipcheck - check IP at intervals to log modem or ip failures
//================================================================

Sleep(30); // wait for mysql to come up at boot time
include('mysql.php');

while (1)
 {
  unset($out);
  exec("rm -f /home/userid/ipcheck/output.data",$out,$rc);
  exec("ping -c 1 -w 5 74.125.21.102 > /home/userid/ipcheck/output.data 2>&1 &",$out,$rc);
  sleep(5);
  exec("grep 'bytes from' /home/userid/ipcheck/output.data | wc -l",$out,$rc);
  $flag = $out[0];
  $time = time();
  db_connect('localhost','dbuser','dbpassword','stats',0);
  db_query("insert into t_stats (s_timestamp,s_flag) values('$time','$flag')",0);
  sleep(5);
 }
?>

So basically what this does is run a ping command to 74.125.21.102 (a google.com server) and wait 5 seconds for the response.  If the ping worked, $flag contains 1, otherwise 0, and we put that value in a mysql table along with the current timestamp.  Then we wait another 5 seconds and go do the whole thing again.

This means we’re checking and recording the google.com ping status every 10 seconds, and the script has been running since June of 2018 so I have quite a collection of data here.

I wrote another script to look for outages, and used that periodically when I thought there was trouble, but it was pretty rare so I just let the data build up without any real thought of usage.  Until lately, when the service seemed to start getting bad.

Around October of 2019 I started noticing more and more internet outages, probably about 5 minutes or less each, but still annoying.  Are things getting worse or am I just being picky?  I need some kind of chart or graph.

Creating Graphs from the Data Collection

In the “old days” I wrote Flash programs to produce graphs on the screen.  You pass a string of variables to the flash program and it displayed nice bar or other graph types.  People even sold these packages for folks who didn’t want to program their own.  But today I need something more HTML-5′ish or javascript, and there’s a free package available called chart.js which seems very good.  I was able to produce this year report, among others, which show my ping failures have definitely been getting worse:

ping_failures_1

Now if I click on one of the bars, let’s say Dec, that shows each day on a month report, and if I click on a Day, it shows each hour on the day report.  These are all the same web page and chart coding, just with php variable differences such as the number of bars.

ping_failures_2

So I can tell that in the last few months of 2019 we had plenty of daily outages.  I can show this to my ISP and complain (as if that will do any good).  I should note that I cannot get fiber to my house, which may be a solution.  Stuck with cable, DSL, or satellite, and cable, even with outages, is by far the best of those choices.

Working with charts.js

Most open-source projects seem to be on github these days, and charts.js is no exception.  So when you go to the chartjs.org web site and click the Github link to download, you’re met with a list of who-knows-what as usual for github.  I don’t want to know all the latest updates.  I just want to download what I need.

Don’t use the green Clone or Download button on github.  While you get a bunch of stuff, you would need to build the final js file yourself and that’s probably no fun.  Instead, click the Releases button near the top and download the Chart.min.js file.  Turns out that’s all you will need.  Put that into a spot accessible by your web page as a java source file.

To create a vertical bar chart, we pass two strings of data to the chart js program, the values for the x axis labels, and the data values for each bar.  The number of items in the labels determine the number of bars.  Here’s an example for the year report shown above:

$label_string = "'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'";
$data_string = "2420,1440,1950,1240,1560,1190,1410,1030,3170,6320,6310,8510"

We’ll use some php logic at the top of a new web page named ping_report.php to create those values depending on the report type (year, month, day, hour) with data loaded from the database we’ve been collecting over the years.  For now though, let’s just look at how the chart is created using those strings.

First, we’ll need to include Chart.min.js in our web page from wherever you stored it, such as this in the <head> section:

<script src="include/Chart.min.js"></script>

Then in the <body> section we’ll need to specify the place we want to draw the chart.  In our case it’s basically the full area of the web page, such as the html below.  I don’t fully understand the width and height.  My output comes out to be around the size of the current browser window, not fixed at 800×500.

<canvas id="barchart" width="800" height="500"></canvas>

Next we get into the chart-making javascript code.  Here is a listing that creates the year report using the variable strings previously mentioned:

<script>
 Chart.defaults.global.animation.duration = 100;
 const month_conversion = {Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11','Dec':'12'};

 new Chart(document.getElementById("barchart"),  
  {
   type: 'bar',
   data: 
    {
     labels: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
     datasets: 
      [
       {
        backgroundColor: "#3e95cd",
        data: [2420,1440,1950,1240,1560,1190,1410,1030,3170,6320,6310,8510],
       }
      ]
    },
   options: 
    {
     legend: { display: false },
     title: { display: false },
     scales: { yAxes: [{ ticks: { beginAtZero: true}} ] },
     events: ['mousemove','click'],
     onClick: function(evt)
      { 
       x = this.getElementAtEvent(evt);
       a = x.length;
       if (a == 1)
        {
         label = this.data.labels[x[0]._index];
         month = month_conversion[label];
         window.location.href = 'ping_report.php?report=month&timestamp=2019/'+month+'/01'
        } 
      },
    }  
  }
 ); 
</script>

So let’s go over some of the javascript as best I can (which is not very well):

The Chart.defaults.global.animation.duration is a global option that speeds up the animation of the bars when drawing them.  By default the bars grow pretty slowly and while neat, it’s way to slow for me.  So experiment with this value after making some charts.

The month_conversion line is an array for converting short month names (Jan, Feb, etc.) to a 2 digit month number.  We’ll need this only for the year report when translating the mouse click to a number to be used for the month chart.

The new Chart line sets up a chart instance using the canvas named barchart.  From that point on we can use this to reference the chart instance.  Next we get to specify variable names and contents used to create a chart.  These are all defined on the various chart.js web pages if you need more information.  type: bar is pretty obvious, and you’ll notice the labels section has only one set of data (one row of x-axis labels), while the dataset section could potentially have more than one set of graph data.  In our case though, we only have one set of data.  The data for both of these items is actually provided by our previously filled php variables such as this clip from our php file:

data: 
 {
  labels: [<?=$label_string?>],
  datasets: 
   [
    {
     backgroundColor: "#3e95cd",
     data: [<?=$data_string?>],
    }
   ]
 },

Next we have various options to be used to create the chart.  Let’s look these over:

options: 
 {
  legend: { display: false },
  title: { display: false },
  scales: { yAxes: [{ ticks: { beginAtZero: true}} ] },
  events: ['mousemove','click'],

The legend and title are set to false because we don’t want the chart system to be adding things to the screen.  We’ll add our own title as needed with normal html.

The scales option indicates that we would like the yAxes to always begin at zero.  Without this option, chart.js will adjust the yAxes numbers based on the data, and the bottom line may not be zero which is confusing to me for this chart.  So we set the beginAtZero option.

The events are things that the chart will listen for.  We set these to mousemove (which will by default show a cute little window of data when you move the mouse) and the click option which allows you to get a response when you click on a bar.

The next option took me a lot of fiddling.  In fact, I could find no sample for this on the internet anywhere and stumbled upon the solution myself without having to look at the chart.js source code, luckily. Since I want to start with a year report and be able to drill down to month, day, and hour reports, that means the bars need to be clickable.

onClick: function(evt)
 { 
  x = this.getElementAtEvent(evt);
  a = x.length;
  if (a == 1)
   {
    label = this.data.labels[x[0]._index];
    month = month_conversion[label];
    window.location.href = 'ping_report.php?report=month&timestamp=2019/'+month+'/01'
   } 
 },

The onClick section defines what to do when we click one of the bars.  First we load variable x with the event structure.  This is some kind of array so it has a length, which I believe is the number of bars clicked.  If the number of bars is not 1, we don’t do anything.

If the number of bars is 1, then we grab the label string related to the bar we clicked.  This is one of the $label_string variables we previously setup which for the year report are Jan, Feb, etc.  For other reports we get a 2 digit number for each label.

With the year report only, we need to run the label through the month_conversion array because we want the result as a 2 digit month number, not the 3 character month name abbreviation.  Then we set our new window location (URL) to call the same php web page once more, but this time with report=month and timestamp=Y/m/d with the month changing depending on what was clicked.  Easy huh?  But it took me some thinking.

Working with PHP Code

I should also describe some of the php code used to generate the variables passed to chart.js, such as:

<?
 include "protect/mysql.php";
 $report = $_GET['report'];
 if (!$report) $report = 'year';
 $timestamp = $_GET['timestamp'];

The include for mysql.php allows me to use generic subroutine calls such as db_connect() in my code, which in this case convert to mysql routines.  This way I can easily switch to mssql or postgre without having to alter my code.  I just change the include.

Next we read the report and timestamp variables from the URL as usual.  If the report variable is blank, we’ll set it to “year” and run a year report.

Next we have a block of code for each report time, setting up variables as needed for each chart type.  This method allows us to use a single php web page for all 4 report types.

switch ($report)
 {
  case 'year':
   if (!$timestamp) $timestamp= date('Y/01/01 00:00:00');
   $start = strtotime(date('Y/01/01 00:00:00',strtotime($timestamp)));
   $year = date('Y',$start);
   $month = date('m',$start);
   $day = date('d',$start);
   $end = strtotime(date("Y/12/31 23:59:59",$start));
   $title = "Outbound Ping Failures in Total Seconds for each Month in $year";
   $col_start = 1;
   $col_end = 12;
   $column_field = 'n';
   $ts = date('Y/01/01',strtotime('-1 year',$start));
   $prev_link = "<a href='ping_report.php?report=year&timestamp=$ts'>Previous Year</a>";
   $ts = date('Y/01/01',strtotime('+1 year',$start));
   $next_link = "<a href='ping_report.php?report=year&timestamp=$ts'>Next Year</a>";
   break;

  case 'month':
   if (!$timestamp) $timestamp= date('Y/m/01 00:00:00');
   $start = strtotime(date('Y/m/01 00:00:00',strtotime($timestamp)));
   $year = date('Y',$start);
   $month = date('m',$start);
   $day = date('d',$start);
   $month_name = date('F',$start);
   $days_in_month = date('t',$start);
   $end = strtotime(date("Y/m/$days_in_month 23:59:59",$start));
   $title = "Outbound Ping Failures in Total Seconds for each Day in $month_name $year";
   $col_start = 1;
   $col_end = $days_in_month;
   $column_field = 'j';
   $ts = date('Y/m/01',strtotime('-1 month',$start));
   $prev_link = "<a href='ping_report.php?report=month&timestamp=$ts'>Previous Month</a>";
   $ts = date('Y/m/01',strtotime('+1 month',$start));
   $next_link = "<a href='ping_report.php?report=month&timestamp=$ts'>Next Month</a>";
   break;

 case 'day':
   if (!$timestamp) $timestamp = date('Y/m/d 00:00:00');
   $start = strtotime(date('Y/m/d 00:00:00',strtotime($timestamp)));
   $year = date('Y',$start);
   $month = date('m',$start);
   $day = date('d',$start);
   $day_name = date('l',$start).', '.date('Y/m/d',$start);
   $end = strtotime(date("Y/m/d 23:59:59",$start));
   $title = "Outbound Ping Failures in Total Seconds for each Hour on $day_name";
   $col_start = 0;
   $col_end = 23;
   $column_field = 'H';
   $ts = date('Y/m/d',strtotime('-1 day',$start));
   $prev_link = "<a href='ping_report.php?report=day&timestamp=$ts'>Previous Day</a>";
   $ts = date('Y/m/d',strtotime('+1 day',$start));
   $next_link = "<a href='ping_report.php?report=day&timestamp=$ts'>Next Day</a>";
   break;

  case 'hour':
   if (!$timestamp) $timestamp = date('Y/m/d H:00:00');
   $start = strtotime(date('Y/m/d H:00:00',strtotime($timestamp)));
   $year = date('Y',$start);
   $month = date('m',$start);
   $day = date('d',$start);
   $day_name = date('l',$start).', '.date('Y/m/d H:00 - H:59',$start);
   $end = strtotime(date("Y/m/d H:59:59",$start));
   $title = "Outbound Ping Failures in Total Seconds for each Minute on $day_name";
   $col_start = 0;
   $col_end = 59;
   $column_field = 'i';
   break;
 }

We’ll also need to generate the $label_string variable, which is responsible for the labels on the X axis, and the number of bars in the chart.  To do that we’ll call a small subroutine:

$label_string = get_label_string($report,$col_start,$col_end);

And that subroutine returns the Jan/Feb/Mar/etc. string for year reports, or a 2 digit number string for other reports (i.e. number of days in month, number of hours in day, or number of minutes in hour).

function get_label_string($report,$col_start,$col_end)
{
 if ($report == 'year') return "'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'";

 $label_string = '';
 for ($i=$col_start;$i<=$col_end;$i++) 
  {
   $label_string .= sprintf(",'%02d'",$i);
   $data[$i] = 0;
  } 
 $label_string = substr($label_string,1);
 return $label_string;
}

And lastly, we need to use the variables previously filled (based on report type) to read the database and add up the ping outage times.

db_connect('localhost','dbuser','dbpassword','stats',1000);
$result = db_query("select * from t_stats where s_flag = '0' and s_timestamp >= '$start' and s_timestamp <= '$end' order by s_timestamp ",1001);

while ($row=db_fetch_array($result))
 {
  list($time) = $row;
  $column = (int) (date($column_field,$time));
  $data[$column]++;
 }

$data_string = '';
for ($i=$col_start;$i<=$col_end;$i++) $data_string .= ','.$data[$i] * 10;
$data_string = substr($data_string,1);

So we connect to our database and read records where the s_flag=0 (outages) that match the s_timestamp range we’re interested in ($start to $end inclusive).

While reading the database, we add up the number of ping outage records by time to match our chart, and store them in the $data array.

When done with the database, we format the $data array into a string that we’ll pass to chart.js.  While formatting, we’ll multiply the value by 10, because our ping script only checks connectivity every 10 seconds.  So for example, for the Day chart example above we end up with a $data_string like this:

400,800,0,0,0,0,0,0,60,130,370,490,30,50,20,810,470,820,840,1360,880,20,0,20,10,360,400,120,50,0,0

Disclaimer

Note that this blog entry is not meant to be a tutorial for beginners.  There’s a whole lot I left out.  But it may be helpful to someone already familiar with php, mysql, and javascript coding.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>