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 */
26function 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
388var sleepSampleInterval = 30 * 1000; // in milliseconds.
389var sleepText = loadTimeData.getString('systemSuspended');
390var invalidDataText = loadTimeData.getString('invalidData');
391var offlineText = loadTimeData.getString('offlineText');
392
393var 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 */
409function 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 */
467function 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 */
536function 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 */
673function 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
789function showCpuIdleData(idleStateData, systemResumedArray) {
790  showStateOccupancyData(idleStateData,
791                         systemResumedArray,
792                         'idleStateOccupancyPercentageHeader',
793                         null,
794                         'cpu-idle-plots-div');
795}
796
797function showCpuFreqData(freqStateData, systemResumedArray) {
798  showStateOccupancyData(freqStateData,
799                         systemResumedArray,
800                         'frequencyStateOccupancyPercentageHeader',
801                         'MHz',
802                         'cpu-freq-plots-div');
803}
804
805function requestBatteryChargeData() {
806  chrome.send('requestBatteryChargeData');
807}
808
809function requestCpuIdleData() {
810  chrome.send('requestCpuIdleData');
811}
812
813function 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 */
828function 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
841var powerUI = {
842  showBatteryChargeData: showBatteryChargeData,
843  showCpuIdleData: showCpuIdleData,
844  showCpuFreqData: showCpuFreqData
845};
846
847document.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