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.
6 * Plot a line graph of data versus time on a HTML canvas element.
8 * @param {HTMLCanvasElement} canvas The canvas on which the line graph is
10 * @param {Array.<number>} tData The time (in seconds) in the past when the
11 * corresponding data in plots was sampled.
12 * @param {Array.<{data: Array.<number>, color: string}>} plots An
13 * array of plots to plot on the canvas. The field 'data' of a plot is an
14 * array of samples to be plotted as a line graph with color speficied by
15 * the field 'color'. The elements in the 'data' array are ordered
16 * corresponding to their sampling time in the argument 'tData'. Also, the
17 * number of elements in the 'data' array should be the same as in the time
18 * array 'tData' above.
19 * @param {number} yMin Minimum bound of y-axis
20 * @param {number} yMax Maximum bound of y-axis.
21 * @param {integer} yPrecision An integer value representing the number of
22 * digits of precision the y-axis data should be printed with.
24 function plotLineGraph(canvas
, tData
, plots
, yMin
, yMax
, yPrecision
) {
25 var textFont
= '12px Arial';
27 var padding
= 5; // Pixels
28 var errorOffsetPixels
= 15;
29 var gridColor
= '#ccc';
30 var ctx
= canvas
.getContext('2d');
31 var size
= tData
.length
;
33 function drawText(text
, x
, y
) {
35 ctx
.fillStyle
= '#000';
36 ctx
.fillText(text
, x
, y
);
39 function printErrorText(text
) {
40 ctx
.clearRect(0, 0, canvas
.width
, canvas
.height
);
41 drawText(text
, errorOffsetPixels
, errorOffsetPixels
);
45 printErrorText(loadTimeData
.getString('notEnoughDataAvailableYet'));
49 for (var count
= 0; count
< plots
.length
; count
++) {
50 if (plots
[count
].data
.length
!= size
) {
51 throw new Error('Mismatch in time and plot data.');
55 function valueToString(value
) {
56 return Number(value
).toPrecision(yPrecision
);
59 function getTextWidth(text
) {
61 // For now, all text is drawn to the left of vertical lines, or centered.
62 // Add a 2 pixel padding so that there is some spacing between the text
63 // and the vertical line.
64 return Math
.round(ctx
.measureText(text
).width
) + 2;
67 function drawHighlightText(text
, x
, y
, color
) {
68 ctx
.strokeStyle
= '#000';
69 ctx
.strokeRect(x
, y
- textHeight
, getTextWidth(text
), textHeight
);
70 ctx
.fillStyle
= color
;
71 ctx
.fillRect(x
, y
- textHeight
, getTextWidth(text
), textHeight
);
72 ctx
.fillStyle
= '#fff';
73 ctx
.fillText(text
, x
, y
);
76 function drawLine(x1
, y1
, x2
, y2
, color
) {
81 ctx
.strokeStyle
= color
;
86 // The strokeRect method of the 2d context of a canvas draws a bounding
87 // rectangle with an offset origin and greater dimensions. Hence, use this
88 // function to draw a rect at the desired location with desired dimensions.
89 function drawRect(x
, y
, width
, height
, color
) {
90 drawLine(x
, y
, x
+ width
- 1, y
, color
);
91 drawLine(x
, y
, x
, y
+ height
- 1, color
);
92 drawLine(x
, y
+ height
- 1, x
+ width
- 1, y
+ height
- 1, color
);
93 drawLine(x
+ width
- 1, y
, x
+ width
- 1, y
+ height
- 1, color
);
96 var yMinStr
= valueToString(yMin
);
97 var yMaxStr
= valueToString(yMax
);
98 var yHalfStr
= valueToString((yMax
+ yMin
) / 2);
99 var yMinWidth
= getTextWidth(yMinStr
);
100 var yMaxWidth
= getTextWidth(yMaxStr
);
101 var yHalfWidth
= getTextWidth(yHalfStr
);
103 var xMinStr
= tData
[0];
104 var xMaxStr
= tData
[size
- 1];
105 var xMinWidth
= getTextWidth(xMinStr
);
106 var xMaxWidth
= getTextWidth(xMaxStr
);
108 var xOrigin
= padding
+ Math
.max(yMinWidth
,
110 Math
.round(xMinWidth
/ 2));
111 var yOrigin
= padding
+ textHeight
;
112 var width
= canvas
.width
- xOrigin
- Math
.floor(xMaxWidth
/ 2) - padding
;
114 canvas
.width
+= size
- width
;
117 var height
= canvas
.height
- yOrigin
- textHeight
- padding
;
119 function drawPlots() {
121 ctx
.clearRect(0, 0, canvas
.width
, canvas
.height
);
123 // Draw the bounding rectangle.
124 drawRect(xOrigin
, yOrigin
, width
, height
, gridColor
);
126 // Draw the x and y bound values.
127 drawText(yMaxStr
, xOrigin
- yMaxWidth
, yOrigin
+ textHeight
);
128 drawText(yMinStr
, xOrigin
- yMinWidth
, yOrigin
+ height
);
129 drawText(xMinStr
, xOrigin
- xMinWidth
/ 2, yOrigin
+ height
+ textHeight
);
131 xOrigin
+ width
- xMaxWidth
/ 2,
132 yOrigin
+ height
+ textHeight
);
134 // Draw y-level (horizontal) lines.
135 drawLine(xOrigin
+ 1, yOrigin
+ height
/ 4,
136 xOrigin
+ width
- 2, yOrigin
+ height
/ 4,
138 drawLine(xOrigin
+ 1, yOrigin
+ height
/ 2,
139 xOrigin
+ width
- 2, yOrigin
+ height
/ 2, gridColor
);
140 drawLine(xOrigin
+ 1, yOrigin
+ 3 * height
/ 4,
141 xOrigin
+ width
- 2, yOrigin
+ 3 * height
/ 4,
144 // Draw half-level value.
146 xOrigin
- yHalfWidth
,
147 yOrigin
+ height
/ 2 + textHeight
/ 2);
150 var yValRange
= yMax
- yMin
;
151 for (var count
= 0; count
< plots
.length
; count
++) {
152 var plot
= plots
[count
];
153 var yData
= plot
.data
;
154 ctx
.strokeStyle
= plot
.color
;
156 var beginPath
= true;
157 for (var i
= 0; i
< size
; i
++) {
159 if (typeof val
=== 'string') {
160 // Stroke the plot drawn so far and begin a fresh plot.
166 var xPos
= xOrigin
+ Math
.floor(i
/ (size
- 1) * (width
- 1));
167 var yPos
= yOrigin
+ height
- 1 -
168 Math
.round((val
- yMin
) / yValRange
* (height
- 1));
170 ctx
.moveTo(xPos
, yPos
);
171 // A simple move to does not print anything. Hence, draw a little
172 // square here to mark a beginning.
173 ctx
.fillStyle
= '#000';
174 ctx
.fillRect(xPos
- 1, yPos
- 1, 2, 2);
177 ctx
.lineTo(xPos
, yPos
);
178 if (i
=== size
- 1 || typeof yData
[i
+ 1] === 'string') {
179 // Draw a little square to mark an end to go with the start
180 // markers from above.
181 ctx
.fillStyle
= '#000';
182 ctx
.fillRect(xPos
- 1, yPos
- 1, 2, 2);
189 // Paint the missing time intervals with |gridColor|.
190 // Pick one of the plots to look for missing time intervals.
191 var inMissingInterval
= false;
193 for (var i
= 0; i
< size
; i
++) {
194 if (typeof plots
[0].data
[i
] === 'string') {
195 if (!inMissingInterval
) {
196 inMissingInterval
= true;
197 // The missing interval should actually start from the previous
199 intervalStart
= Math
.max(i
- 1, 0);
201 } else if (inMissingInterval
) {
202 inMissingInterval
= false;
203 var xLeft
= xOrigin
+
204 Math
.floor(intervalStart
/ (size
- 1) * (width
- 1));
205 var xRight
= xOrigin
+ Math
.floor(i
/ (size
- 1) * (width
- 1));
206 ctx
.fillStyle
= gridColor
;
207 // The x offsets below are present so that the blank space starts
208 // and ends between two valid samples.
209 ctx
.fillRect(xLeft
+ 1, yOrigin
, xRight
- xLeft
- 2, height
- 1);
214 function drawTimeGuide(tDataIndex
) {
215 var x
= xOrigin
+ tDataIndex
/ (size
- 1) * (width
- 1);
216 drawLine(x
, yOrigin
, x
, yOrigin
+ height
- 1, '#000');
217 drawText(tData
[tDataIndex
],
218 x
- getTextWidth(tData
[tDataIndex
]) / 2,
221 for (var count
= 0; count
< plots
.length
; count
++) {
222 var yData
= plots
[count
].data
;
224 // Draw small black square on the plot where the time guide intersects
226 var val
= yData
[tDataIndex
];
228 if (typeof val
=== 'string') {
229 yPos
= yOrigin
+ Math
.round(height
/ 2);
232 yPos
= yOrigin
+ height
- 1 -
233 Math
.round((val
- yMin
) / (yMax
- yMin
) * (height
- 1));
234 valStr
= valueToString(val
);
236 ctx
.fillStyle
= '#000';
237 ctx
.fillRect(x
- 2, yPos
- 2, 4, 4);
239 // Draw the val to right of the intersection.
241 if (yPos
- textHeight
/ 2 < yOrigin
) {
242 yLoc
= yOrigin
+ textHeight
;
243 } else if (yPos
+ textHeight
/ 2 >= yPos
+ height
) {
244 yLoc
= yOrigin
+ height
- 1;
246 yLoc
= yPos
+ textHeight
/ 2;
248 drawHighlightText(valStr
, x
+ 5, yLoc
, plots
[count
].color
);
252 function onMouseOverOrMove(event
) {
255 var boundingRect
= canvas
.getBoundingClientRect();
256 var x
= event
.clientX
- boundingRect
.left
;
257 var y
= event
.clientY
- boundingRect
.top
;
258 if (x
< xOrigin
|| x
>= xOrigin
+ width
||
259 y
< yOrigin
|| y
>= yOrigin
+ height
) {
264 drawTimeGuide(x
- xOrigin
);
266 drawTimeGuide(Math
.round((x
- xOrigin
) / (width
- 1) * (size
- 1)));
270 function onMouseOut(event
) {
275 canvas
.addEventListener('mouseover', onMouseOverOrMove
);
276 canvas
.addEventListener('mousemove', onMouseOverOrMove
);
277 canvas
.addEventListener('mouseout', onMouseOut
);
280 var sleepSampleInterval
= 30 * 1000; // in milliseconds.
281 var sleepText
= loadTimeData
.getString('systemSuspended');
284 * Add samples in |sampleArray| to individual plots in |plots|. If the system
285 * resumed from a sleep/suspend, then "suspended" sleep samples are added to
286 * the plot for the sleep duration.
288 * @param {Array.<{data: Array.<number>, color: string}>} plots An
289 * array of plots to plot on the canvas. The field 'data' of a plot is an
290 * array of samples to be plotted as a line graph with color speficied by
291 * the field 'color'. The elements in the 'data' array are ordered
292 * corresponding to their sampling time in the argument 'tData'. Also, the
293 * number of elements in the 'data' array should be the same as in the time
294 * array 'tData' below.
295 * @param {Array.<number>} tData The time (in seconds) in the past when the
296 * corresponding data in plots was sampled.
297 * @param {Array.<number>} sampleArray The array of samples wherein each
298 * element corresponds to the individual plot in |plots|.
299 * @param {number} sampleTime Time in milliseconds since the epoch when the
300 * samples in |sampleArray| were captured.
301 * @param {number} previousSampleTime Time in milliseconds since the epoch
302 * when the sample prior to the current sample was captured.
303 * @param {Array.<{time: number, sleepDuration: number}>} systemResumedArray An
304 * array objects corresponding to system resume events. The 'time' field is
305 * for the time in milliseconds since the epoch when the system resumed. The
306 * 'sleepDuration' field is for the time in milliseconds the system spent
307 * in sleep/suspend state.
309 function addTimeDataSample(plots
, tData
, sampleArray
,
310 sampleTime
, previousSampleTime
,
311 systemResumedArray
) {
312 for (var i
= 0; i
< plots
.length
; i
++) {
313 if (plots
[i
].data
.length
!= tData
.length
) {
314 throw new Error('Mismatch in time and plot data.');
319 if (tData
.length
== 0) {
320 time
= new Date(sampleTime
);
321 tData
[0] = time
.toLocaleTimeString();
322 for (var i
= 0; i
< plots
.length
; i
++) {
323 plots
[i
].data
[0] = sampleArray
[i
];
328 for (var i
= 0; i
< systemResumedArray
.length
; i
++) {
329 var resumeTime
= systemResumedArray
[i
].time
;
330 var sleepDuration
= systemResumedArray
[i
].sleepDuration
;
331 var sleepStartTime
= resumeTime
- sleepDuration
;
332 if (resumeTime
< sampleTime
&& sleepStartTime
> previousSampleTime
) {
333 // Add sleep samples for every |sleepSampleInterval|.
334 var sleepSampleTime
= sleepStartTime
;
335 while (sleepSampleTime
< resumeTime
) {
336 time
= new Date(sleepSampleTime
);
337 tData
.push(time
.toLocaleTimeString());
338 for (var j
= 0; j
< plots
.length
; j
++) {
339 plots
[j
].data
.push(sleepText
);
341 sleepSampleTime
+= sleepSampleInterval
;
346 time
= new Date(sampleTime
);
347 tData
.push(time
.toLocaleTimeString());
348 for (var i
= 0; i
< plots
.length
; i
++) {
349 plots
[i
].data
.push(sampleArray
[i
]);
354 * Display the battery charge vs time on a line graph.
356 * @param {Array.<{time: number,
357 * batteryPercent: number,
358 * batteryDischargeRate: number,
359 * externalPower: number}>} powerSupplyArray An array of objects
360 * with fields representing the battery charge, time when the charge
361 * measurement was taken, and whether there was external power connected at
363 * @param {Array.<{time: ?, sleepDuration: ?}>} systemResumedArray An array
364 * objects with fields 'time' and 'sleepDuration'. Each object corresponds
365 * to a system resume event. The 'time' field is for the time in
366 * milliseconds since the epoch when the system resumed. The 'sleepDuration'
367 * field is for the time in milliseconds the system spent in sleep/suspend
370 function showBatteryChargeData(powerSupplyArray
, systemResumedArray
) {
371 var chargeTimeData
= [];
378 var dischargeRateTimeData
= [];
379 var dischargeRatePlot
= [
385 var minDischargeRate
= 1000; // A high unrealistic number to begin with.
386 var maxDischargeRate
= -1000; // A low unrealistic number to begin with.
387 for (var i
= 0; i
< powerSupplyArray
.length
; i
++) {
388 var j
= Math
.max(i
- 1, 0);
390 addTimeDataSample(chargePlot
, chargeTimeData
,
391 [powerSupplyArray
[i
].batteryPercent
],
392 powerSupplyArray
[i
].time
,
393 powerSupplyArray
[j
].time
,
396 var dischargeRate
= powerSupplyArray
[i
].batteryDischargeRate
;
397 minDischargeRate
= Math
.min(dischargeRate
, minDischargeRate
);
398 maxDischargeRate
= Math
.max(dischargeRate
, maxDischargeRate
);
399 addTimeDataSample(dischargeRatePlot
,
400 dischargeRateTimeData
,
402 powerSupplyArray
[i
].time
,
403 powerSupplyArray
[j
].time
,
406 if (minDischargeRate
== maxDischargeRate
) {
407 // This means that all the samples had the same value. Hence, offset the
408 // extremes by a bit so that the plot looks good.
409 minDischargeRate
-= 1;
410 maxDischargeRate
+= 1;
413 var chargeCanvas
= $('battery-charge-percentage-canvas');
414 var dischargeRateCanvas
= $('battery-discharge-rate-canvas');
415 plotLineGraph(chargeCanvas
, chargeTimeData
, chargePlot
, 0.00, 100.00, 3);
416 plotLineGraph(dischargeRateCanvas
,
417 dischargeRateTimeData
,
424 function requestBatteryChargeData() {
425 chrome
.send('requestBatteryChargeData');
429 showBatteryChargeData
: showBatteryChargeData
432 document
.addEventListener('DOMContentLoaded', function() {
433 requestBatteryChargeData();
434 $('battery-charge-reload-button').onclick
= requestBatteryChargeData
;