Home | History | Annotate | Download | only in chromeos
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * Plot a line graph of data versus time on a HTML canvas element.
      7  *
      8  * @param {HTMLCanvasElement} plotCanvas The canvas on which the line graph is
      9  *     drawn.
     10  * @param {HTMLCanvasElement} legendCanvas The canvas on which the legend for
     11  *     the line graph is drawn.
     12  * @param {Array.<number>} tData The time (in seconds) in the past when the
     13  *     corresponding data in plots was sampled.
     14  * @param {Array.<{data: Array.<number>, color: string}>} plots An
     15  *     array of plots to plot on the canvas. The field 'data' of a plot is an
     16  *     array of samples to be plotted as a line graph with color speficied by
     17  *     the field 'color'. The elements in the 'data' array are ordered
     18  *     corresponding to their sampling time in the argument 'tData'. Also, the
     19  *     number of elements in the 'data' array should be the same as in the time
     20  *     array 'tData' above.
     21  * @param {number} yMin Minimum bound of y-axis
     22  * @param {number} yMax Maximum bound of y-axis.
     23  * @param {integer} yPrecision An integer value representing the number of
     24  *     digits of precision the y-axis data should be printed with.
     25  */
     26 function plotLineGraph(
     27     plotCanvas, legendCanvas, tData, plots, yMin, yMax, yPrecision) {
     28   var textFont = 12 * devicePixelRatio + 'px Arial';
     29   var textHeight = 12 * devicePixelRatio;
     30   var padding = 5 * devicePixelRatio;  // Pixels
     31   var errorOffsetPixels = 15 * devicePixelRatio;
     32   var gridColor = '#ccc';
     33   var plotCtx = plotCanvas.getContext('2d');
     34   var size = tData.length;
     35 
     36   function drawText(ctx, text, x, y) {
     37     ctx.font = textFont;
     38     ctx.fillStyle = '#000';
     39     ctx.fillText(text, x, y);
     40   }
     41 
     42   function printErrorText(ctx, text) {
     43     ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
     44     drawText(ctx, text, errorOffsetPixels, errorOffsetPixels);
     45   }
     46 
     47   if (size < 2) {
     48     printErrorText(plotCtx,
     49                    loadTimeData.getString('notEnoughDataAvailableYet'));
     50     return;
     51   }
     52 
     53   for (var count = 0; count < plots.length; count++) {
     54     if (plots[count].data.length != size) {
     55       throw new Error('Mismatch in time and plot data.');
     56     }
     57   }
     58 
     59   function valueToString(value) {
     60     if (Math.abs(value) < 1) {
     61       return Number(value).toFixed(yPrecision - 1);
     62     } else {
     63       return Number(value).toPrecision(yPrecision);
     64     }
     65   }
     66 
     67   function getTextWidth(ctx, text) {
     68     ctx.font = textFont;
     69     // For now, all text is drawn to the left of vertical lines, or centered.
     70     // Add a 2 pixel padding so that there is some spacing between the text
     71     // and the vertical line.
     72     return Math.round(ctx.measureText(text).width) + 2 * devicePixelRatio;
     73   }
     74 
     75   function getLegend(text) {
     76     return ' ' + text + '    ';
     77   }
     78 
     79   function drawHighlightText(ctx, text, x, y, color) {
     80     ctx.strokeStyle = '#000';
     81     ctx.strokeRect(x, y - textHeight, getTextWidth(ctx, text), textHeight);
     82     ctx.fillStyle = color;
     83     ctx.fillRect(x, y - textHeight, getTextWidth(ctx, text), textHeight);
     84     ctx.fillStyle = '#fff';
     85     ctx.fillText(text, x, y);
     86   }
     87 
     88   function drawLine(ctx, x1, y1, x2, y2, color) {
     89     ctx.save();
     90     ctx.beginPath();
     91     ctx.moveTo(x1, y1);
     92     ctx.lineTo(x2, y2);
     93     ctx.strokeStyle = color;
     94     ctx.lineWidth = 1 * devicePixelRatio;
     95     ctx.stroke();
     96     ctx.restore();
     97   }
     98 
     99   // The strokeRect method of the 2d context of a plotCanvas draws a bounding
    100   // rectangle with an offset origin and greater dimensions. Hence, use this
    101   // function to draw a rect at the desired location with desired dimensions.
    102   function drawRect(ctx, x, y, width, height, color) {
    103     var offset = 1 * devicePixelRatio;
    104     drawLine(ctx, x, y, x + width - offset, y, color);
    105     drawLine(ctx, x, y, x, y + height - offset, color);
    106     drawLine(ctx, x, y + height - offset, x + width - offset,
    107         y + height - offset, color);
    108     drawLine(ctx, x + width - offset, y, x + width - offset,
    109         y + height - offset, color);
    110   }
    111 
    112   function drawLegend() {
    113     // Show a legend only if at least one individual plot has a name.
    114     var valid = false;
    115     for (var i = 0; i < plots.length; i++) {
    116       if (plots[i].name != null) {
    117         valid = true;
    118         break;
    119       }
    120     }
    121     if (!valid) {
    122       legendCanvas.hidden = true;
    123       return;
    124     }
    125 
    126 
    127     var padding = 2 * devicePixelRatio;
    128     var legendSquareSide = 12 * devicePixelRatio;
    129     var legendCtx = legendCanvas.getContext('2d');
    130     var xLoc = padding;
    131     var yLoc = padding;
    132     // Adjust the height of the canvas before drawing on it.
    133     for (var i = 0; i < plots.length; i++) {
    134       if (plots[i].name == null) {
    135         continue;
    136       }
    137       var legendText = getLegend(plots[i].name);
    138       xLoc += legendSquareSide + getTextWidth(legendCtx, legendText) +
    139               2 * padding;
    140       if (i < plots.length - 1) {
    141         var xLocNext = xLoc +
    142                        getTextWidth(legendCtx, getLegend(plots[i + 1].name)) +
    143                        legendSquareSide;
    144         if (xLocNext >= legendCanvas.width) {
    145           xLoc = padding;
    146           yLoc = yLoc + 2 * padding + textHeight;
    147         }
    148       }
    149     }
    150 
    151     legendCanvas.height = yLoc + textHeight + padding;
    152     legendCanvas.style.height =
    153         legendCanvas.height / devicePixelRatio + 'px';
    154 
    155     xLoc = padding;
    156     yLoc = padding;
    157     // Go over the plots again, this time drawing the legends.
    158     for (var i = 0; i < plots.length; i++) {
    159       legendCtx.fillStyle = plots[i].color;
    160       legendCtx.fillRect(xLoc, yLoc, legendSquareSide, legendSquareSide);
    161       xLoc += legendSquareSide;
    162 
    163       var legendText = getLegend(plots[i].name);
    164       drawText(legendCtx, legendText, xLoc, yLoc + textHeight - 1);
    165       xLoc += getTextWidth(legendCtx, legendText) + 2 * padding;
    166 
    167       if (i < plots.length - 1) {
    168         var xLocNext = xLoc +
    169                        getTextWidth(legendCtx, getLegend(plots[i + 1].name)) +
    170                        legendSquareSide;
    171         if (xLocNext >= legendCanvas.width) {
    172           xLoc = padding;
    173           yLoc = yLoc + 2 * padding + textHeight;
    174         }
    175       }
    176     }
    177   }
    178 
    179   var yMinStr = valueToString(yMin);
    180   var yMaxStr = valueToString(yMax);
    181   var yHalfStr = valueToString((yMax + yMin) / 2);
    182   var yMinWidth = getTextWidth(plotCtx, yMinStr);
    183   var yMaxWidth = getTextWidth(plotCtx, yMaxStr);
    184   var yHalfWidth = getTextWidth(plotCtx, yHalfStr);
    185 
    186   var xMinStr = tData[0];
    187   var xMaxStr = tData[size - 1];
    188   var xMinWidth = getTextWidth(plotCtx, xMinStr);
    189   var xMaxWidth = getTextWidth(plotCtx, xMaxStr);
    190 
    191   var xOrigin = padding + Math.max(yMinWidth,
    192                                    yMaxWidth,
    193                                    Math.round(xMinWidth / 2));
    194   var yOrigin = padding + textHeight;
    195   var width = plotCanvas.width - xOrigin - Math.floor(xMaxWidth / 2) - padding;
    196   if (width < size) {
    197     plotCanvas.width += size - width;
    198     width = size;
    199   }
    200   var height = plotCanvas.height - yOrigin - textHeight - padding;
    201   var linePlotEndMarkerWidth = 3;
    202 
    203   function drawPlots() {
    204     // Start fresh.
    205     plotCtx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
    206 
    207     // Draw the bounding rectangle.
    208     drawRect(plotCtx, xOrigin, yOrigin, width, height, gridColor);
    209 
    210     // Draw the x and y bound values.
    211     drawText(plotCtx, yMaxStr, xOrigin - yMaxWidth, yOrigin + textHeight);
    212     drawText(plotCtx, yMinStr, xOrigin - yMinWidth, yOrigin + height);
    213     drawText(plotCtx,
    214              xMinStr,
    215              xOrigin - xMinWidth / 2,
    216              yOrigin + height + textHeight);
    217     drawText(plotCtx,
    218              xMaxStr,
    219              xOrigin + width - xMaxWidth / 2,
    220              yOrigin + height + textHeight);
    221 
    222     // Draw y-level (horizontal) lines.
    223     drawLine(plotCtx,
    224              xOrigin + 1, yOrigin + height / 4,
    225              xOrigin + width - 2, yOrigin + height / 4,
    226              gridColor);
    227     drawLine(plotCtx,
    228              xOrigin + 1, yOrigin + height / 2,
    229              xOrigin + width - 2, yOrigin + height / 2, gridColor);
    230     drawLine(plotCtx,
    231              xOrigin + 1, yOrigin + 3 * height / 4,
    232              xOrigin + width - 2, yOrigin + 3 * height / 4,
    233              gridColor);
    234 
    235     // Draw half-level value.
    236     drawText(plotCtx,
    237              yHalfStr,
    238              xOrigin - yHalfWidth,
    239              yOrigin + height / 2 + textHeight / 2);
    240 
    241     // Draw the plots.
    242     var yValRange = yMax - yMin;
    243     for (var count = 0; count < plots.length; count++) {
    244       var plot = plots[count];
    245       var yData = plot.data;
    246       plotCtx.strokeStyle = plot.color;
    247       plotCtx.lineWidth = 2;
    248       plotCtx.beginPath();
    249       var beginPath = true;
    250       for (var i = 0; i < size; i++) {
    251         var val = yData[i];
    252         if (typeof val === 'string') {
    253           // Stroke the plot drawn so far and begin a fresh plot.
    254           plotCtx.stroke();
    255           plotCtx.beginPath();
    256           beginPath = true;
    257           continue;
    258         }
    259         var xPos = xOrigin + Math.floor(i / (size - 1) * (width - 1));
    260         var yPos = yOrigin + height - 1 -
    261                    Math.round((val - yMin) / yValRange * (height - 1));
    262         if (beginPath) {
    263           plotCtx.moveTo(xPos, yPos);
    264           // A simple move to does not print anything. Hence, draw a little
    265           // square here to mark a beginning.
    266           plotCtx.fillStyle = '#000';
    267           plotCtx.fillRect(xPos - linePlotEndMarkerWidth,
    268                            yPos - linePlotEndMarkerWidth,
    269                            linePlotEndMarkerWidth * devicePixelRatio,
    270                            linePlotEndMarkerWidth * devicePixelRatio);
    271           beginPath = false;
    272         } else {
    273           plotCtx.lineTo(xPos, yPos);
    274           if (i === size - 1 || typeof yData[i + 1] === 'string') {
    275             // Draw a little square to mark an end to go with the start
    276             // markers from above.
    277             plotCtx.fillStyle = '#000';
    278             plotCtx.fillRect(xPos - linePlotEndMarkerWidth,
    279                              yPos - linePlotEndMarkerWidth,
    280                              linePlotEndMarkerWidth * devicePixelRatio,
    281                              linePlotEndMarkerWidth * devicePixelRatio);
    282           }
    283         }
    284       }
    285       plotCtx.stroke();
    286     }
    287 
    288     // Paint the missing time intervals with |gridColor|.
    289     // Pick one of the plots to look for missing time intervals.
    290     function drawMissingRect(start, end) {
    291       var xLeft = xOrigin + Math.floor(start / (size - 1) * (width - 1));
    292       var xRight = xOrigin + Math.floor(end / (size - 1) * (width - 1));
    293       plotCtx.fillStyle = gridColor;
    294       // The x offsets below are present so that the blank space starts
    295       // and ends between two valid samples.
    296       plotCtx.fillRect(xLeft + 1, yOrigin, xRight - xLeft - 2, height - 1);
    297     }
    298     var inMissingInterval = false;
    299     var intervalStart;
    300     for (var i = 0; i < size; i++) {
    301       if (typeof plots[0].data[i] === 'string') {
    302         if (!inMissingInterval) {
    303           inMissingInterval = true;
    304           // The missing interval should actually start from the previous
    305           // sample.
    306           intervalStart = Math.max(i - 1, 0);
    307         }
    308 
    309         if (i == size - 1) {
    310           // If this is the last sample, just draw missing rect.
    311           drawMissingRect(intervalStart, i);
    312         }
    313       } else if (inMissingInterval) {
    314         inMissingInterval = false;
    315         drawMissingRect(intervalStart, i);
    316       }
    317     }
    318   }
    319 
    320   function drawTimeGuide(tDataIndex) {
    321     var x = xOrigin + tDataIndex / (size - 1) * (width - 1);
    322     drawLine(plotCtx, x, yOrigin, x, yOrigin + height - 1, '#000');
    323     drawText(plotCtx,
    324              tData[tDataIndex],
    325              x - getTextWidth(plotCtx, tData[tDataIndex]) / 2,
    326              yOrigin - 2);
    327 
    328     for (var count = 0; count < plots.length; count++) {
    329       var yData = plots[count].data;
    330 
    331       // Draw small black square on the plot where the time guide intersects
    332       // it.
    333       var val = yData[tDataIndex];
    334       var yPos, valStr;
    335       if (typeof val === 'string') {
    336         yPos = yOrigin + Math.round(height / 2);
    337         valStr = val;
    338       } else {
    339         yPos = yOrigin + height - 1 -
    340             Math.round((val - yMin) / (yMax - yMin) * (height - 1));
    341         valStr = valueToString(val);
    342       }
    343       plotCtx.fillStyle = '#000';
    344       plotCtx.fillRect(x - 2, yPos - 2, 4, 4);
    345 
    346       // Draw the val to right of the intersection.
    347       var yLoc;
    348       if (yPos - textHeight / 2 < yOrigin) {
    349         yLoc = yOrigin + textHeight;
    350       } else if (yPos + textHeight / 2 >= yPos + height) {
    351         yLoc = yOrigin + height - 1;
    352       } else {
    353         yLoc = yPos + textHeight / 2;
    354       }
    355       drawHighlightText(plotCtx, valStr, x + 5, yLoc, plots[count].color);
    356     }
    357   }
    358 
    359   function onMouseOverOrMove(event) {
    360     drawPlots();
    361 
    362     var boundingRect = plotCanvas.getBoundingClientRect();
    363     var x = Math.round((event.clientX - boundingRect.left) * devicePixelRatio);
    364     var y = Math.round((event.clientY - boundingRect.top) * devicePixelRatio);
    365     if (x < xOrigin || x >= xOrigin + width ||
    366         y < yOrigin || y >= yOrigin + height) {
    367       return;
    368     }
    369 
    370     if (width == size) {
    371       drawTimeGuide(x - xOrigin);
    372     } else {
    373       drawTimeGuide(Math.round((x - xOrigin) / (width - 1) * (size - 1)));
    374     }
    375   }
    376 
    377   function onMouseOut(event) {
    378     drawPlots();
    379   }
    380 
    381   drawLegend();
    382   drawPlots();
    383   plotCanvas.addEventListener('mouseover', onMouseOverOrMove);
    384   plotCanvas.addEventListener('mousemove', onMouseOverOrMove);
    385   plotCanvas.addEventListener('mouseout', onMouseOut);
    386 }
    387 
    388 var sleepSampleInterval = 30 * 1000; // in milliseconds.
    389 var sleepText = loadTimeData.getString('systemSuspended');
    390 var invalidDataText = loadTimeData.getString('invalidData');
    391 var offlineText = loadTimeData.getString('offlineText');
    392 
    393 var plotColors = ['Red', 'Blue', 'Green', 'Gold', 'CadetBlue', 'LightCoral',
    394                   'LightSlateGray', 'Peru', 'DarkRed', 'LawnGreen', 'Tan'];
    395 
    396 /**
    397  * Add canvases for plotting to |plotsDiv|. For every header in |headerArray|,
    398  * one canvas for the plot and one for its legend are added.
    399  *
    400  * @param {Array.<string>} headerArray Headers for the different plots to be
    401  *     added to |plotsDiv|.
    402  * @param {HTMLDivElement} plotsDiv The div element into which the canvases
    403  *     are added.
    404  * @return {<string>: {plotCanvas: <HTMLCanvasElement>,
    405  *                     legendCanvas: <HTMLCanvasElement>} Returns an object
    406  *    with the headers as 'keys'. Each element is an object containing the
    407  *    legend canvas and the plot canvas that have been added to |plotsDiv|.
    408  */
    409 function addCanvases(headerArray, plotsDiv) {
    410   // Remove the contents before adding new ones.
    411   while (plotsDiv.firstChild != null) {
    412     plotsDiv.removeChild(plotsDiv.firstChild);
    413   }
    414   var width = Math.floor(plotsDiv.getBoundingClientRect().width);
    415   var canvases = {};
    416   for (var i = 0; i < headerArray.length; i++) {
    417     var header = document.createElement('h4');
    418     header.textContent = headerArray[i];
    419     plotsDiv.appendChild(header);
    420 
    421     var legendCanvas = document.createElement('canvas');
    422     legendCanvas.width = width * devicePixelRatio;
    423     legendCanvas.style.width = width + 'px';
    424     plotsDiv.appendChild(legendCanvas);
    425 
    426     var plotCanvasDiv = document.createElement('div');
    427     plotCanvasDiv.style.overflow = 'auto';
    428     plotsDiv.appendChild(plotCanvasDiv);
    429 
    430     plotCanvas = document.createElement('canvas');
    431     plotCanvas.width = width * devicePixelRatio;
    432     plotCanvas.height = 200 * devicePixelRatio;
    433     plotCanvas.style.height = '200px';
    434     plotCanvasDiv.appendChild(plotCanvas);
    435 
    436     canvases[headerArray[i]] = {plot: plotCanvas, legend: legendCanvas};
    437   }
    438   return canvases;
    439 }
    440 
    441 /**
    442  * Add samples in |sampleArray| to individual plots in |plots|. If the system
    443  * resumed from a sleep/suspend, then "suspended" sleep samples are added to
    444  * the plot for the sleep duration.
    445  *
    446  * @param {Array.<{data: Array.<number>, color: string}>} plots An
    447  *     array of plots to plot on the canvas. The field 'data' of a plot is an
    448  *     array of samples to be plotted as a line graph with color speficied by
    449  *     the field 'color'. The elements in the 'data' array are ordered
    450  *     corresponding to their sampling time in the argument 'tData'. Also, the
    451  *     number of elements in the 'data' array should be the same as in the time
    452  *     array 'tData' below.
    453  * @param {Array.<number>} tData The time (in seconds) in the past when the
    454  *     corresponding data in plots was sampled.
    455  * @param {Array.<number>} sampleArray The array of samples wherein each
    456  *     element corresponds to the individual plot in |plots|.
    457  * @param {number} sampleTime Time in milliseconds since the epoch when the
    458  *     samples in |sampleArray| were captured.
    459  * @param {number} previousSampleTime Time in milliseconds since the epoch
    460  *     when the sample prior to the current sample was captured.
    461  * @param {Array.<{time: number, sleepDuration: number}>} systemResumedArray An
    462  *     array objects corresponding to system resume events. The 'time' field is
    463  *     for the time in milliseconds since the epoch when the system resumed. The
    464  *     'sleepDuration' field is for the time in milliseconds the system spent
    465  *     in sleep/suspend state.
    466  */
    467 function addTimeDataSample(plots, tData, absTime, sampleArray,
    468                            sampleTime, previousSampleTime,
    469                            systemResumedArray) {
    470   for (var i = 0; i < plots.length; i++) {
    471     if (plots[i].data.length != tData.length) {
    472       throw new Error('Mismatch in time and plot data.');
    473     }
    474   }
    475 
    476   var time;
    477   if (tData.length == 0) {
    478     time = new Date(sampleTime);
    479     absTime[0] = sampleTime;
    480     tData[0] = time.toLocaleTimeString();
    481     for (var i = 0; i < plots.length; i++) {
    482       plots[i].data[0] = sampleArray[i];
    483     }
    484     return;
    485   }
    486 
    487   for (var i = 0; i < systemResumedArray.length; i++) {
    488     var resumeTime = systemResumedArray[i].time;
    489     var sleepDuration = systemResumedArray[i].sleepDuration;
    490     var sleepStartTime = resumeTime - sleepDuration;
    491     if (resumeTime < sampleTime) {
    492       if (sleepStartTime < previousSampleTime) {
    493         // This can happen if pending callbacks were handled before actually
    494         // suspending.
    495         sleepStartTime = previousSampleTime + 1000;
    496       }
    497       // Add sleep samples for every |sleepSampleInterval|.
    498       var sleepSampleTime = sleepStartTime;
    499       while (sleepSampleTime < resumeTime) {
    500         time = new Date(sleepSampleTime);
    501         absTime.push(sleepSampleTime);
    502         tData.push(time.toLocaleTimeString());
    503         for (var j = 0; j < plots.length; j++) {
    504           plots[j].data.push(sleepText);
    505         }
    506         sleepSampleTime += sleepSampleInterval;
    507       }
    508     }
    509   }
    510 
    511   time = new Date(sampleTime);
    512   absTime.push(sampleTime);
    513   tData.push(time.toLocaleTimeString());
    514   for (var i = 0; i < plots.length; i++) {
    515     plots[i].data.push(sampleArray[i]);
    516   }
    517 }
    518 
    519 /**
    520  * Display the battery charge vs time on a line graph.
    521  *
    522  * @param {Array.<{time: number,
    523  *                 batteryPercent: number,
    524  *                 batteryDischargeRate: number,
    525  *                 externalPower: number}>} powerSupplyArray An array of objects
    526  *     with fields representing the battery charge, time when the charge
    527  *     measurement was taken, and whether there was external power connected at
    528  *     that time.
    529  * @param {Array.<{time: ?, sleepDuration: ?}>} systemResumedArray An array
    530  *     objects with fields 'time' and 'sleepDuration'. Each object corresponds
    531  *     to a system resume event. The 'time' field is for the time in
    532  *     milliseconds since the epoch when the system resumed. The 'sleepDuration'
    533  *     field is for the time in milliseconds the system spent in sleep/suspend
    534  *     state.
    535  */
    536 function showBatteryChargeData(powerSupplyArray, systemResumedArray) {
    537   var chargeTimeData = [];
    538   var chargeAbsTime = [];
    539   var chargePlot = [
    540     {
    541       name: loadTimeData.getString('batteryChargePercentageHeader'),
    542       color: 'Blue',
    543       data: []
    544     }
    545   ];
    546   var dischargeRateTimeData = [];
    547   var dischargeRateAbsTime = [];
    548   var dischargeRatePlot = [
    549     {
    550       name: loadTimeData.getString('dischargeRateLegendText'),
    551       color: 'Red',
    552       data: []
    553     },
    554     {
    555       name: loadTimeData.getString('movingAverageLegendText'),
    556       color: 'Green',
    557       data: []
    558     },
    559     {
    560       name: loadTimeData.getString('binnedAverageLegendText'),
    561       color: 'Blue',
    562       data: []
    563     }
    564   ];
    565   var minDischargeRate = 1000;  // A high unrealistic number to begin with.
    566   var maxDischargeRate = -1000; // A low unrealistic number to begin with.
    567   for (var i = 0; i < powerSupplyArray.length; i++) {
    568     var j = Math.max(i - 1, 0);
    569 
    570     addTimeDataSample(chargePlot,
    571                       chargeTimeData,
    572                       chargeAbsTime,
    573                       [powerSupplyArray[i].batteryPercent],
    574                       powerSupplyArray[i].time,
    575                       powerSupplyArray[j].time,
    576                       systemResumedArray);
    577 
    578     var dischargeRate = powerSupplyArray[i].batteryDischargeRate;
    579     var inputSampleCount = $('sample-count-input').value;
    580 
    581     var movingAverage = 0;
    582     var k = 0;
    583     for (k = 0; k < inputSampleCount && i - k >= 0; k++) {
    584       movingAverage += powerSupplyArray[i - k].batteryDischargeRate;
    585     }
    586     // |k| will be atleast 1 because the 'min' value of the input field is 1.
    587     movingAverage /= k;
    588 
    589     var binnedAverage = 0;
    590     for (k = 0; k < inputSampleCount; k++) {
    591       var currentSampleIndex = i - i % inputSampleCount + k;
    592       if (currentSampleIndex >= powerSupplyArray.length) {
    593         break;
    594       }
    595 
    596       binnedAverage +=
    597           powerSupplyArray[currentSampleIndex].batteryDischargeRate;
    598     }
    599     binnedAverage /= k;
    600 
    601     minDischargeRate = Math.min(dischargeRate, minDischargeRate);
    602     maxDischargeRate = Math.max(dischargeRate, maxDischargeRate);
    603     addTimeDataSample(dischargeRatePlot,
    604                       dischargeRateTimeData,
    605                       dischargeRateAbsTime,
    606                       [dischargeRate, movingAverage, binnedAverage],
    607                       powerSupplyArray[i].time,
    608                       powerSupplyArray[j].time,
    609                       systemResumedArray);
    610   }
    611   if (minDischargeRate == maxDischargeRate) {
    612     // This means that all the samples had the same value. Hence, offset the
    613     // extremes by a bit so that the plot looks good.
    614     minDischargeRate -= 1;
    615     maxDischargeRate += 1;
    616   }
    617 
    618   plotsDiv = $('battery-charge-plots-div');
    619 
    620   canvases = addCanvases(
    621       [loadTimeData.getString('batteryChargePercentageHeader'),
    622        loadTimeData.getString('batteryDischargeRateHeader')],
    623       plotsDiv);
    624 
    625   batteryChargeCanvases = canvases[
    626       loadTimeData.getString('batteryChargePercentageHeader')];
    627   plotLineGraph(
    628       batteryChargeCanvases['plot'],
    629       batteryChargeCanvases['legend'],
    630       chargeTimeData,
    631       chargePlot,
    632       0.00,
    633       100.00,
    634       3);
    635 
    636   dischargeRateCanvases = canvases[
    637       loadTimeData.getString('batteryDischargeRateHeader')];
    638   plotLineGraph(
    639       dischargeRateCanvases['plot'],
    640       dischargeRateCanvases['legend'],
    641       dischargeRateTimeData,
    642       dischargeRatePlot,
    643       minDischargeRate,
    644       maxDischargeRate,
    645       3);
    646 }
    647 
    648 /**
    649  * Shows state occupancy data (CPU idle or CPU freq state occupancy) on a set of
    650  * plots on the about:power UI.
    651  *
    652  * @param {Array.<{Array.<{
    653  *     time: number,
    654  *     cpuOnline:boolean,
    655  *     timeInState: {<string>: number}>}>} timeInStateData Array of arrays
    656  *     where each array corresponds to a CPU on the system. The elements of the
    657  *     individual arrays contain state occupancy samples.
    658  * @param {Array.<{time: ?, sleepDuration: ?}>} systemResumedArray An array
    659  *     objects with fields 'time' and 'sleepDuration'. Each object corresponds
    660  *     to a system resume event. The 'time' field is for the time in
    661  *     milliseconds since the epoch when the system resumed. The 'sleepDuration'
    662  *     field is for the time in milliseconds the system spent in sleep/suspend
    663  *     state.
    664  * @param {string} i18nHeaderString The header string to be displayed with each
    665  *     plot. For example, CPU idle data will have its own header format, and CPU
    666  *     freq data will have its header format.
    667  * @param {string} unitString This is the string capturing the unit, if any,
    668  *     for the different states. Note that this is not the unit of the data
    669  *     being plotted.
    670  * @param {HTMLDivElement} plotsDivId The div element in which the plots should
    671  *     be added.
    672  */
    673 function showStateOccupancyData(timeInStateData,
    674                                 systemResumedArray,
    675                                 i18nHeaderString,
    676                                 unitString,
    677                                 plotsDivId) {
    678   var cpuPlots = [];
    679   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
    680     var cpuData = timeInStateData[cpu];
    681     if (cpuData.length == 0) {
    682       cpuPlots[cpu] = {plots: [], tData: []};
    683       continue;
    684     }
    685     tData = [];
    686     absTime = [];
    687     // Each element of |plots| is an array of samples, one for each of the CPU
    688     // states. The number of states is dicovered by looking at the first
    689     // sample for which the CPU is online.
    690     var plots = [];
    691     var stateIndexMap = [];
    692     var stateCount = 0;
    693     for (var i = 0; i < cpuData.length; i++) {
    694       if (cpuData[i].cpuOnline) {
    695         for (var state in cpuData[i].timeInState) {
    696           var stateName = state;
    697           if (unitString != null) {
    698             stateName += ' ' + unitString;
    699           }
    700           plots.push({
    701               name: stateName,
    702               data: [],
    703               color: plotColors[stateCount]
    704           });
    705           stateIndexMap.push(state);
    706           stateCount += 1;
    707         }
    708         break;
    709       }
    710     }
    711     // If stateCount is 0, then it means the CPU has been offline
    712     // throughout. Just add a single plot for such a case.
    713     if (stateCount == 0) {
    714       plots.push({
    715           name: null,
    716           data: [],
    717           color: null
    718       });
    719       stateCount = 1; // Some invalid state!
    720     }
    721 
    722     // Pass the samples through the function addTimeDataSample to add 'sleep'
    723     // samples.
    724     for (var i = 0; i < cpuData.length; i++) {
    725       var sample = cpuData[i];
    726       var valArray = [];
    727       for (var j = 0; j < stateCount; j++) {
    728         if (sample.cpuOnline) {
    729           valArray[j] = sample.timeInState[stateIndexMap[j]];
    730         } else {
    731           valArray[j] = offlineText;
    732         }
    733       }
    734 
    735       var k = Math.max(i - 1, 0);
    736       addTimeDataSample(plots,
    737                         tData,
    738                         absTime,
    739                         valArray,
    740                         sample.time,
    741                         cpuData[k].time,
    742                         systemResumedArray);
    743     }
    744 
    745     // Calculate the percentage occupancy of each state. A valid number is
    746     // possible only if two consecutive samples are valid/numbers.
    747     for (var k = 0; k < stateCount; k++) {
    748       var stateData = plots[k].data;
    749       // Skip the first sample as there is no previous sample.
    750       for (var i = stateData.length - 1; i > 0; i--) {
    751         if (typeof stateData[i] === 'number') {
    752           if (typeof stateData[i - 1] === 'number') {
    753             stateData[i] = (stateData[i] - stateData[i - 1]) /
    754                            (absTime[i] - absTime[i - 1]) * 100;
    755           } else {
    756             stateData[i] = invalidDataText;
    757           }
    758         }
    759       }
    760     }
    761 
    762     // Remove the first sample from the time and data arrays.
    763     tData.shift();
    764     for (var k = 0; k < stateCount; k++) {
    765       plots[k].data.shift();
    766     }
    767     cpuPlots[cpu] = {plots: plots, tData: tData};
    768   }
    769 
    770   headers = [];
    771   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
    772     headers[cpu] =
    773         'CPU ' + cpu + ' ' + loadTimeData.getString(i18nHeaderString);
    774   }
    775 
    776   canvases = addCanvases(headers, $(plotsDivId));
    777   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
    778     cpuCanvases = canvases[headers[cpu]];
    779     plotLineGraph(cpuCanvases['plot'],
    780                   cpuCanvases['legend'],
    781                   cpuPlots[cpu]['tData'],
    782                   cpuPlots[cpu]['plots'],
    783                   0,
    784                   100,
    785                   3);
    786   }
    787 }
    788 
    789 function showCpuIdleData(idleStateData, systemResumedArray) {
    790   showStateOccupancyData(idleStateData,
    791                          systemResumedArray,
    792                          'idleStateOccupancyPercentageHeader',
    793                          null,
    794                          'cpu-idle-plots-div');
    795 }
    796 
    797 function showCpuFreqData(freqStateData, systemResumedArray) {
    798   showStateOccupancyData(freqStateData,
    799                          systemResumedArray,
    800                          'frequencyStateOccupancyPercentageHeader',
    801                          'MHz',
    802                          'cpu-freq-plots-div');
    803 }
    804 
    805 function requestBatteryChargeData() {
    806   chrome.send('requestBatteryChargeData');
    807 }
    808 
    809 function requestCpuIdleData() {
    810   chrome.send('requestCpuIdleData');
    811 }
    812 
    813 function requestCpuFreqData() {
    814   chrome.send('requestCpuFreqData');
    815 }
    816 
    817 /**
    818  * Return a callback for the 'Show'/'Hide' buttons for each section of the
    819  * about:power page.
    820  *
    821  * @param {string} sectionId The ID of the section which is to be shown or
    822  *     hidden.
    823  * @param {string} buttonId The ID of the 'Show'/'Hide' button.
    824  * @param {function} requestFunction The function which should be invoked on
    825  *    'Show' to request for data from chrome.
    826  * @return {function} The button callback function.
    827  */
    828 function showHideCallback(sectionId, buttonId, requestFunction) {
    829   return function() {
    830     if ($(sectionId).hidden) {
    831       $(sectionId).hidden = false;
    832       $(buttonId).textContent = loadTimeData.getString('hideButton');
    833       requestFunction();
    834     } else {
    835       $(sectionId).hidden = true;
    836       $(buttonId).textContent = loadTimeData.getString('showButton');
    837     }
    838   }
    839 }
    840 
    841 var powerUI = {
    842   showBatteryChargeData: showBatteryChargeData,
    843   showCpuIdleData: showCpuIdleData,
    844   showCpuFreqData: showCpuFreqData
    845 };
    846 
    847 document.addEventListener('DOMContentLoaded', function() {
    848   $('battery-charge-section').hidden = true;
    849   $('battery-charge-show-button').onclick = showHideCallback(
    850       'battery-charge-section',
    851       'battery-charge-show-button',
    852       requestBatteryChargeData);
    853   $('battery-charge-reload-button').onclick = requestBatteryChargeData;
    854   $('sample-count-input').onclick = requestBatteryChargeData;
    855 
    856   $('cpu-idle-section').hidden = true;
    857   $('cpu-idle-show-button').onclick = showHideCallback(
    858       'cpu-idle-section', 'cpu-idle-show-button', requestCpuIdleData);
    859   $('cpu-idle-reload-button').onclick = requestCpuIdleData;
    860 
    861   $('cpu-freq-section').hidden = true;
    862   $('cpu-freq-show-button').onclick = showHideCallback(
    863       'cpu-freq-section', 'cpu-freq-show-button', requestCpuFreqData);
    864   $('cpu-freq-reload-button').onclick = requestCpuFreqData;
    865 });
    866