Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / power.js
blob98c38de54c0b7092adcd2eb0d8d67e2e13128193
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.
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;
36   function drawText(ctx, text, x, y) {
37     ctx.font = textFont;
38     ctx.fillStyle = '#000';
39     ctx.fillText(text, x, y);
40   }
42   function printErrorText(ctx, text) {
43     ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
44     drawText(ctx, text, errorOffsetPixels, errorOffsetPixels);
45   }
47   if (size < 2) {
48     printErrorText(plotCtx,
49                    loadTimeData.getString('notEnoughDataAvailableYet'));
50     return;
51   }
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   }
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   }
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   }
75   function getLegend(text) {
76     return ' ' + text + '    ';
77   }
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   }
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   }
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   }
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     }
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     }
151     legendCanvas.height = yLoc + textHeight + padding;
152     legendCanvas.style.height =
153         legendCanvas.height / devicePixelRatio + 'px';
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;
163       var legendText = getLegend(plots[i].name);
164       drawText(legendCtx, legendText, xLoc, yLoc + textHeight - 1);
165       xLoc += getTextWidth(legendCtx, legendText) + 2 * padding;
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   }
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);
186   var xMinStr = tData[0];
187   var xMaxStr = tData[size - 1];
188   var xMinWidth = getTextWidth(plotCtx, xMinStr);
189   var xMaxWidth = getTextWidth(plotCtx, xMaxStr);
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;
203   function drawPlots() {
204     // Start fresh.
205     plotCtx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
207     // Draw the bounding rectangle.
208     drawRect(plotCtx, xOrigin, yOrigin, width, height, gridColor);
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);
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);
235     // Draw half-level value.
236     drawText(plotCtx,
237              yHalfStr,
238              xOrigin - yHalfWidth,
239              yOrigin + height / 2 + textHeight / 2);
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     }
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         }
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   }
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);
328     for (var count = 0; count < plots.length; count++) {
329       var yData = plots[count].data;
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);
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   }
359   function onMouseOverOrMove(event) {
360     drawPlots();
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     }
370     if (width == size) {
371       drawTimeGuide(x - xOrigin);
372     } else {
373       drawTimeGuide(Math.round((x - xOrigin) / (width - 1) * (size - 1)));
374     }
375   }
377   function onMouseOut(event) {
378     drawPlots();
379   }
381   drawLegend();
382   drawPlots();
383   plotCanvas.addEventListener('mouseover', onMouseOverOrMove);
384   plotCanvas.addEventListener('mousemove', onMouseOverOrMove);
385   plotCanvas.addEventListener('mouseout', onMouseOut);
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');
393 var plotColors = ['Red', 'Blue', 'Green', 'Gold', 'CadetBlue', 'LightCoral',
394                   'LightSlateGray', 'Peru', 'DarkRed', 'LawnGreen', 'Tan'];
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.
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);
421     var legendCanvas = document.createElement('canvas');
422     legendCanvas.width = width * devicePixelRatio;
423     legendCanvas.style.width = width + 'px';
424     plotsDiv.appendChild(legendCanvas);
426     var plotCanvasDiv = document.createElement('div');
427     plotCanvasDiv.style.overflow = 'auto';
428     plotsDiv.appendChild(plotCanvasDiv);
430     plotCanvas = document.createElement('canvas');
431     plotCanvas.width = width * devicePixelRatio;
432     plotCanvas.height = 200 * devicePixelRatio;
433     plotCanvas.style.height = '200px';
434     plotCanvasDiv.appendChild(plotCanvas);
436     canvases[headerArray[i]] = {plot: plotCanvas, legend: legendCanvas};
437   }
438   return canvases;
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.
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>} absTime
456  * @param {Array<number>} sampleArray The array of samples wherein each
457  *     element corresponds to the individual plot in |plots|.
458  * @param {number} sampleTime Time in milliseconds since the epoch when the
459  *     samples in |sampleArray| were captured.
460  * @param {number} previousSampleTime Time in milliseconds since the epoch
461  *     when the sample prior to the current sample was captured.
462  * @param {Array<{time: number, sleepDuration: number}>} systemResumedArray An
463  *     array objects corresponding to system resume events. The 'time' field is
464  *     for the time in milliseconds since the epoch when the system resumed. The
465  *     'sleepDuration' field is for the time in milliseconds the system spent
466  *     in sleep/suspend state.
467  */
468 function addTimeDataSample(plots, tData, absTime, sampleArray,
469                            sampleTime, previousSampleTime,
470                            systemResumedArray) {
471   for (var i = 0; i < plots.length; i++) {
472     if (plots[i].data.length != tData.length) {
473       throw new Error('Mismatch in time and plot data.');
474     }
475   }
477   var time;
478   if (tData.length == 0) {
479     time = new Date(sampleTime);
480     absTime[0] = sampleTime;
481     tData[0] = time.toLocaleTimeString();
482     for (var i = 0; i < plots.length; i++) {
483       plots[i].data[0] = sampleArray[i];
484     }
485     return;
486   }
488   for (var i = 0; i < systemResumedArray.length; i++) {
489     var resumeTime = systemResumedArray[i].time;
490     var sleepDuration = systemResumedArray[i].sleepDuration;
491     var sleepStartTime = resumeTime - sleepDuration;
492     if (resumeTime < sampleTime) {
493       if (sleepStartTime < previousSampleTime) {
494         // This can happen if pending callbacks were handled before actually
495         // suspending.
496         sleepStartTime = previousSampleTime + 1000;
497       }
498       // Add sleep samples for every |sleepSampleInterval|.
499       var sleepSampleTime = sleepStartTime;
500       while (sleepSampleTime < resumeTime) {
501         time = new Date(sleepSampleTime);
502         absTime.push(sleepSampleTime);
503         tData.push(time.toLocaleTimeString());
504         for (var j = 0; j < plots.length; j++) {
505           plots[j].data.push(sleepText);
506         }
507         sleepSampleTime += sleepSampleInterval;
508       }
509     }
510   }
512   time = new Date(sampleTime);
513   absTime.push(sampleTime);
514   tData.push(time.toLocaleTimeString());
515   for (var i = 0; i < plots.length; i++) {
516     plots[i].data.push(sampleArray[i]);
517   }
521  * Display the battery charge vs time on a line graph.
523  * @param {Array<{time: number,
524  *                 batteryPercent: number,
525  *                 batteryDischargeRate: number,
526  *                 externalPower: number}>} powerSupplyArray An array of objects
527  *     with fields representing the battery charge, time when the charge
528  *     measurement was taken, and whether there was external power connected at
529  *     that time.
530  * @param {Array<{time: ?, sleepDuration: ?}>} systemResumedArray An array
531  *     objects with fields 'time' and 'sleepDuration'. Each object corresponds
532  *     to a system resume event. The 'time' field is for the time in
533  *     milliseconds since the epoch when the system resumed. The 'sleepDuration'
534  *     field is for the time in milliseconds the system spent in sleep/suspend
535  *     state.
536  */
537 function showBatteryChargeData(powerSupplyArray, systemResumedArray) {
538   var chargeTimeData = [];
539   var chargeAbsTime = [];
540   var chargePlot = [
541     {
542       name: loadTimeData.getString('batteryChargePercentageHeader'),
543       color: 'Blue',
544       data: []
545     }
546   ];
547   var dischargeRateTimeData = [];
548   var dischargeRateAbsTime = [];
549   var dischargeRatePlot = [
550     {
551       name: loadTimeData.getString('dischargeRateLegendText'),
552       color: 'Red',
553       data: []
554     },
555     {
556       name: loadTimeData.getString('movingAverageLegendText'),
557       color: 'Green',
558       data: []
559     },
560     {
561       name: loadTimeData.getString('binnedAverageLegendText'),
562       color: 'Blue',
563       data: []
564     }
565   ];
566   var minDischargeRate = 1000;  // A high unrealistic number to begin with.
567   var maxDischargeRate = -1000; // A low unrealistic number to begin with.
568   for (var i = 0; i < powerSupplyArray.length; i++) {
569     var j = Math.max(i - 1, 0);
571     addTimeDataSample(chargePlot,
572                       chargeTimeData,
573                       chargeAbsTime,
574                       [powerSupplyArray[i].batteryPercent],
575                       powerSupplyArray[i].time,
576                       powerSupplyArray[j].time,
577                       systemResumedArray);
579     var dischargeRate = powerSupplyArray[i].batteryDischargeRate;
580     var inputSampleCount = $('sample-count-input').value;
582     var movingAverage = 0;
583     var k = 0;
584     for (k = 0; k < inputSampleCount && i - k >= 0; k++) {
585       movingAverage += powerSupplyArray[i - k].batteryDischargeRate;
586     }
587     // |k| will be atleast 1 because the 'min' value of the input field is 1.
588     movingAverage /= k;
590     var binnedAverage = 0;
591     for (k = 0; k < inputSampleCount; k++) {
592       var currentSampleIndex = i - i % inputSampleCount + k;
593       if (currentSampleIndex >= powerSupplyArray.length) {
594         break;
595       }
597       binnedAverage +=
598           powerSupplyArray[currentSampleIndex].batteryDischargeRate;
599     }
600     binnedAverage /= k;
602     minDischargeRate = Math.min(dischargeRate, minDischargeRate);
603     maxDischargeRate = Math.max(dischargeRate, maxDischargeRate);
604     addTimeDataSample(dischargeRatePlot,
605                       dischargeRateTimeData,
606                       dischargeRateAbsTime,
607                       [dischargeRate, movingAverage, binnedAverage],
608                       powerSupplyArray[i].time,
609                       powerSupplyArray[j].time,
610                       systemResumedArray);
611   }
612   if (minDischargeRate == maxDischargeRate) {
613     // This means that all the samples had the same value. Hence, offset the
614     // extremes by a bit so that the plot looks good.
615     minDischargeRate -= 1;
616     maxDischargeRate += 1;
617   }
619   plotsDiv = $('battery-charge-plots-div');
621   canvases = addCanvases(
622       [loadTimeData.getString('batteryChargePercentageHeader'),
623        loadTimeData.getString('batteryDischargeRateHeader')],
624       plotsDiv);
626   batteryChargeCanvases = canvases[
627       loadTimeData.getString('batteryChargePercentageHeader')];
628   plotLineGraph(
629       batteryChargeCanvases['plot'],
630       batteryChargeCanvases['legend'],
631       chargeTimeData,
632       chargePlot,
633       0.00,
634       100.00,
635       3);
637   dischargeRateCanvases = canvases[
638       loadTimeData.getString('batteryDischargeRateHeader')];
639   plotLineGraph(
640       dischargeRateCanvases['plot'],
641       dischargeRateCanvases['legend'],
642       dischargeRateTimeData,
643       dischargeRatePlot,
644       minDischargeRate,
645       maxDischargeRate,
646       3);
650  * Shows state occupancy data (CPU idle or CPU freq state occupancy) on a set of
651  * plots on the about:power UI.
653  * @param {Array<Array<{
654  *     time: number,
655  *     cpuOnline: boolean,
656  *     timeInState: Object<number>}>} timeInStateData Array of arrays
657  *     where each array corresponds to a CPU on the system. The elements of the
658  *     individual arrays contain state occupancy samples.
659  * @param {Array<{time: ?, sleepDuration: ?}>} systemResumedArray An array
660  *     objects with fields 'time' and 'sleepDuration'. Each object corresponds
661  *     to a system resume event. The 'time' field is for the time in
662  *     milliseconds since the epoch when the system resumed. The 'sleepDuration'
663  *     field is for the time in milliseconds the system spent in sleep/suspend
664  *     state.
665  * @param {string} i18nHeaderString The header string to be displayed with each
666  *     plot. For example, CPU idle data will have its own header format, and CPU
667  *     freq data will have its header format.
668  * @param {string} unitString This is the string capturing the unit, if any,
669  *     for the different states. Note that this is not the unit of the data
670  *     being plotted.
671  * @param {HTMLDivElement} plotsDivId The div element in which the plots should
672  *     be added.
673  */
674 function showStateOccupancyData(timeInStateData,
675                                 systemResumedArray,
676                                 i18nHeaderString,
677                                 unitString,
678                                 plotsDivId) {
679   var cpuPlots = [];
680   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
681     var cpuData = timeInStateData[cpu];
682     if (cpuData.length == 0) {
683       cpuPlots[cpu] = {plots: [], tData: []};
684       continue;
685     }
686     tData = [];
687     absTime = [];
688     // Each element of |plots| is an array of samples, one for each of the CPU
689     // states. The number of states is dicovered by looking at the first
690     // sample for which the CPU is online.
691     var plots = [];
692     var stateIndexMap = [];
693     var stateCount = 0;
694     for (var i = 0; i < cpuData.length; i++) {
695       if (cpuData[i].cpuOnline) {
696         for (var state in cpuData[i].timeInState) {
697           var stateName = state;
698           if (unitString != null) {
699             stateName += ' ' + unitString;
700           }
701           plots.push({
702               name: stateName,
703               data: [],
704               color: plotColors[stateCount]
705           });
706           stateIndexMap.push(state);
707           stateCount += 1;
708         }
709         break;
710       }
711     }
712     // If stateCount is 0, then it means the CPU has been offline
713     // throughout. Just add a single plot for such a case.
714     if (stateCount == 0) {
715       plots.push({
716           name: null,
717           data: [],
718           color: null
719       });
720       stateCount = 1; // Some invalid state!
721     }
723     // Pass the samples through the function addTimeDataSample to add 'sleep'
724     // samples.
725     for (var i = 0; i < cpuData.length; i++) {
726       var sample = cpuData[i];
727       var valArray = [];
728       for (var j = 0; j < stateCount; j++) {
729         if (sample.cpuOnline) {
730           valArray[j] = sample.timeInState[stateIndexMap[j]];
731         } else {
732           valArray[j] = offlineText;
733         }
734       }
736       var k = Math.max(i - 1, 0);
737       addTimeDataSample(plots,
738                         tData,
739                         absTime,
740                         valArray,
741                         sample.time,
742                         cpuData[k].time,
743                         systemResumedArray);
744     }
746     // Calculate the percentage occupancy of each state. A valid number is
747     // possible only if two consecutive samples are valid/numbers.
748     for (var k = 0; k < stateCount; k++) {
749       var stateData = plots[k].data;
750       // Skip the first sample as there is no previous sample.
751       for (var i = stateData.length - 1; i > 0; i--) {
752         if (typeof stateData[i] === 'number') {
753           if (typeof stateData[i - 1] === 'number') {
754             stateData[i] = (stateData[i] - stateData[i - 1]) /
755                            (absTime[i] - absTime[i - 1]) * 100;
756           } else {
757             stateData[i] = invalidDataText;
758           }
759         }
760       }
761     }
763     // Remove the first sample from the time and data arrays.
764     tData.shift();
765     for (var k = 0; k < stateCount; k++) {
766       plots[k].data.shift();
767     }
768     cpuPlots[cpu] = {plots: plots, tData: tData};
769   }
771   headers = [];
772   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
773     headers[cpu] =
774         'CPU ' + cpu + ' ' + loadTimeData.getString(i18nHeaderString);
775   }
777   canvases = addCanvases(headers, $(plotsDivId));
778   for (var cpu = 0; cpu < timeInStateData.length; cpu++) {
779     cpuCanvases = canvases[headers[cpu]];
780     plotLineGraph(cpuCanvases['plot'],
781                   cpuCanvases['legend'],
782                   cpuPlots[cpu]['tData'],
783                   cpuPlots[cpu]['plots'],
784                   0,
785                   100,
786                   3);
787   }
790 function showCpuIdleData(idleStateData, systemResumedArray) {
791   showStateOccupancyData(idleStateData,
792                          systemResumedArray,
793                          'idleStateOccupancyPercentageHeader',
794                          null,
795                          'cpu-idle-plots-div');
798 function showCpuFreqData(freqStateData, systemResumedArray) {
799   showStateOccupancyData(freqStateData,
800                          systemResumedArray,
801                          'frequencyStateOccupancyPercentageHeader',
802                          'MHz',
803                          'cpu-freq-plots-div');
806 function requestBatteryChargeData() {
807   chrome.send('requestBatteryChargeData');
810 function requestCpuIdleData() {
811   chrome.send('requestCpuIdleData');
814 function requestCpuFreqData() {
815   chrome.send('requestCpuFreqData');
819  * Return a callback for the 'Show'/'Hide' buttons for each section of the
820  * about:power page.
822  * @param {string} sectionId The ID of the section which is to be shown or
823  *     hidden.
824  * @param {string} buttonId The ID of the 'Show'/'Hide' button.
825  * @param {function} requestFunction The function which should be invoked on
826  *    'Show' to request for data from chrome.
827  * @return {function} The button callback function.
828  */
829 function showHideCallback(sectionId, buttonId, requestFunction) {
830   return function() {
831     if ($(sectionId).hidden) {
832       $(sectionId).hidden = false;
833       $(buttonId).textContent = loadTimeData.getString('hideButton');
834       requestFunction();
835     } else {
836       $(sectionId).hidden = true;
837       $(buttonId).textContent = loadTimeData.getString('showButton');
838     }
839   }
842 var powerUI = {
843   showBatteryChargeData: showBatteryChargeData,
844   showCpuIdleData: showCpuIdleData,
845   showCpuFreqData: showCpuFreqData
848 document.addEventListener('DOMContentLoaded', function() {
849   $('battery-charge-section').hidden = true;
850   $('battery-charge-show-button').onclick = showHideCallback(
851       'battery-charge-section',
852       'battery-charge-show-button',
853       requestBatteryChargeData);
854   $('battery-charge-reload-button').onclick = requestBatteryChargeData;
855   $('sample-count-input').onclick = requestBatteryChargeData;
857   $('cpu-idle-section').hidden = true;
858   $('cpu-idle-show-button').onclick = showHideCallback(
859       'cpu-idle-section', 'cpu-idle-show-button', requestCpuIdleData);
860   $('cpu-idle-reload-button').onclick = requestCpuIdleData;
862   $('cpu-freq-section').hidden = true;
863   $('cpu-freq-show-button').onclick = showHideCallback(
864       'cpu-freq-section', 'cpu-freq-show-button', requestCpuFreqData);
865   $('cpu-freq-reload-button').onclick = requestCpuFreqData;