1 // Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview This file contains the DygraphRangeSelector class used to provide
6 * a timeline range selector widget for dygraphs.
10 * The DygraphRangeSelector class provides a timeline range selector widget.
11 * @param {Dygraph} dygraph The dygraph object
14 DygraphRangeSelector
= function(dygraph
) {
15 this.isIE_
= /MSIE/.test(navigator
.userAgent
) && !window
.opera
;
16 this.isUsingExcanvas
= typeof(G_vmlCanvasManager
) != 'undefined';
17 this.dygraph_
= dygraph
;
18 this.createCanvases_();
19 this.createZoomHandles_();
20 this.initInteraction_();
24 * Adds the range selector to the dygraph.
25 * @param {Object} graphDiv The container div for the range selector.
26 * @param {DygraphLayout} layout The DygraphLayout object for this graph.
28 DygraphRangeSelector
.prototype.addToGraph
= function(graphDiv
, layout
) {
29 this.layout_
= layout
;
31 graphDiv
.appendChild(this.bgcanvas_
);
32 graphDiv
.appendChild(this.fgcanvas_
);
33 graphDiv
.appendChild(this.leftZoomHandle_
);
34 graphDiv
.appendChild(this.rightZoomHandle_
);
38 * Renders the static background portion of the range selector.
40 DygraphRangeSelector
.prototype.renderStaticLayer
= function() {
42 this.drawStaticLayer_();
46 * Renders the interactive foreground portion of the range selector.
48 DygraphRangeSelector
.prototype.renderInteractiveLayer
= function() {
49 if (this.isChangingRange_
) {
53 // The zoom handle image may not be loaded yet. May need to try again later.
54 if (this.leftZoomHandle_
.height
== 0 && this.leftZoomHandle_
.retryCount
!= 5) {
56 setTimeout(function() { self
.renderInteractiveLayer(); }, 300);
57 var retryCount
= this.leftZoomHandle_
.retryCount
;
58 this.leftZoomHandle_
.retryCount
= retryCount
== undefined
? 1 : retryCount
+1;
62 this.placeZoomHandles_();
63 this.drawInteractiveLayer_();
68 * Resizes the range selector.
70 DygraphRangeSelector
.prototype.resize_
= function() {
71 function setCanvasRect(canvas
, rect
) {
72 canvas
.style
.top
= rect
.y
+ 'px';
73 canvas
.style
.left
= rect
.x
+ 'px';
74 canvas
.width
= rect
.w
;
75 canvas
.height
= rect
.h
;
76 canvas
.style
.width
= canvas
.width
+ 'px'; // for IE
77 canvas
.style
.height
= canvas
.height
+ 'px'; // for IE
80 var plotArea
= this.layout_
.getPlotArea();
81 var xAxisLabelHeight
= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
84 y
: plotArea
.y
+ plotArea
.h
+ xAxisLabelHeight
+ 4,
86 h
: this.attr_('rangeSelectorHeight')
89 setCanvasRect(this.bgcanvas_
, this.canvasRect_
);
90 setCanvasRect(this.fgcanvas_
, this.canvasRect_
);
93 DygraphRangeSelector
.prototype.attr_
= function(name
) {
94 return this.dygraph_
.attr_(name
);
99 * Creates the background and foreground canvases.
101 DygraphRangeSelector
.prototype.createCanvases_
= function() {
102 this.bgcanvas_
= Dygraph
.createCanvas();
103 this.bgcanvas_
.className
= 'dygraph-rangesel-bgcanvas';
104 this.bgcanvas_
.style
.position
= 'absolute';
105 this.bgcanvas_ctx_
= Dygraph
.getContext(this.bgcanvas_
);
107 this.fgcanvas_
= Dygraph
.createCanvas();
108 this.fgcanvas_
.className
= 'dygraph-rangesel-fgcanvas';
109 this.fgcanvas_
.style
.position
= 'absolute';
110 this.fgcanvas_
.style
.cursor
= 'default';
111 this.fgcanvas_ctx_
= Dygraph
.getContext(this.fgcanvas_
);
116 * Creates the zoom handle elements.
118 DygraphRangeSelector
.prototype.createZoomHandles_
= function() {
119 var img
= new Image();
120 img
.className
= 'dygraph-rangesel-zoomhandle';
121 img
.style
.position
= 'absolute';
122 img
.style
.visibility
= 'hidden'; // Initially hidden so they don't show up in the wrong place.
123 img
.style
.cursor
= 'col-resize';
124 img
.src
= 'data:image/png;base64,\
125 iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
126 zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
127 bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
128 6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
129 qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
131 this.leftZoomHandle_
= img
;
132 this.rightZoomHandle_
= img
.cloneNode(false);
137 * Sets up the interaction for the range selector.
139 DygraphRangeSelector
.prototype.initInteraction_
= function() {
141 var topElem
= this.isIE_
? document
: window
;
144 var isZooming
= false;
145 var isPanning
= false;
147 function toXDataWindow(zoomHandleStatus
) {
148 var xDataLimits
= self
.dygraph_
.xAxisExtremes();
149 var fact
= (xDataLimits
[1] - xDataLimits
[0])/self
.canvasRect_
.w
;
150 var xDataMin
= xDataLimits
[0] + (zoomHandleStatus
.leftHandlePos
- self
.canvasRect_
.x
)*fact
;
151 var xDataMax
= xDataLimits
[0] + (zoomHandleStatus
.rightHandlePos
- self
.canvasRect_
.x
)*fact
;
152 return [xDataMin
, xDataMax
];
155 function onZoomStart(e
) {
156 Dygraph
.cancelEvent(e
);
159 handle
= e
.target
? e
.target
: e
.srcElement
;
160 Dygraph
.addEvent(topElem
, 'mousemove', onZoom
);
161 Dygraph
.addEvent(topElem
, 'mouseup', onZoomEnd
);
162 self
.fgcanvas_
.style
.cursor
= 'col-resize';
169 var delX
= e
.screenX
- xLast
;
170 if (Math
.abs(delX
) < 4) {
174 var zoomHandleStatus
= self
.getZoomHandleStatus_();
175 var halfHandleWidth
= Math
.round(handle
.width
/2);
176 if (handle
== self
.leftZoomHandle_
) {
177 var newPos
= zoomHandleStatus
.leftHandlePos
+ delX
;
178 newPos
= Math
.min(newPos
, zoomHandleStatus
.rightHandlePos
- handle
.width
- 3);
179 newPos
= Math
.max(newPos
, self
.canvasRect_
.x
);
181 var newPos
= zoomHandleStatus
.rightHandlePos
+ delX
;
182 newPos
= Math
.min(newPos
, self
.canvasRect_
.x
+ self
.canvasRect_
.w
);
183 newPos
= Math
.max(newPos
, zoomHandleStatus
.leftHandlePos
+ handle
.width
+ 3);
185 handle
.style
.left
= (newPos
- halfHandleWidth
) + 'px';
186 self
.drawInteractiveLayer_();
188 // Zoom on the fly (if not using excanvas).
189 if (!self
.isUsingExcanvas
) {
194 function onZoomEnd(e
) {
199 Dygraph
.removeEvent(topElem
, 'mousemove', onZoom
);
200 Dygraph
.removeEvent(topElem
, 'mouseup', onZoomEnd
);
201 self
.fgcanvas_
.style
.cursor
= 'default';
203 // If using excanvas, Zoom now.
204 if (self
.isUsingExcanvas
) {
211 var zoomHandleStatus
= self
.getZoomHandleStatus_();
212 self
.isChangingRange_
= true;
213 if (!zoomHandleStatus
.isZoomed
) {
214 self
.dygraph_
.doUnzoom_();
216 var xDataWindow
= toXDataWindow(zoomHandleStatus
);
217 self
.dygraph_
.doZoomXDates_(xDataWindow
[0], xDataWindow
[1]);
220 self
.isChangingRange_
= false;
224 function isMouseInPanZone(e
) {
225 // Getting clientX directly from the event is not accurate enough :(
226 var clientX
= self
.canvasRect_
.x
+ (e
.layerX
!= undefined
? e
.layerX
: e
.offsetX
);
227 var zoomHandleStatus
= self
.getZoomHandleStatus_();
228 return (clientX
> zoomHandleStatus
.leftHandlePos
&& clientX
< zoomHandleStatus
.rightHandlePos
);
231 function onPanStart(e
) {
232 if (!isPanning
&& isMouseInPanZone(e
) && self
.getZoomHandleStatus_().isZoomed
) {
233 Dygraph
.cancelEvent(e
);
236 Dygraph
.addEvent(topElem
, 'mousemove', onPan
);
237 Dygraph
.addEvent(topElem
, 'mouseup', onPanEnd
);
246 var delX
= e
.screenX
- xLast
;
247 if (Math
.abs(delX
) < 4) {
253 var zoomHandleStatus
= self
.getZoomHandleStatus_();
254 var leftHandlePos
= zoomHandleStatus
.leftHandlePos
;
255 var rightHandlePos
= zoomHandleStatus
.rightHandlePos
;
256 var rangeSize
= rightHandlePos
- leftHandlePos
;
257 if (leftHandlePos
+ delX
<= self
.canvasRect_
.x
) {
258 leftHandlePos
= self
.canvasRect_
.x
;
259 rightHandlePos
= leftHandlePos
+ rangeSize
;
260 } else if (rightHandlePos
+ delX
>= self
.canvasRect_
.x
+ self
.canvasRect_
.w
) {
261 rightHandlePos
= self
.canvasRect_
.x
+ self
.canvasRect_
.w
;
262 leftHandlePos
= rightHandlePos
- rangeSize
;
264 leftHandlePos
+= delX
;
265 rightHandlePos
+= delX
;
267 var halfHandleWidth
= Math
.round(self
.leftZoomHandle_
.width
/2);
268 self
.leftZoomHandle_
.style
.left
= (leftHandlePos
- halfHandleWidth
) + 'px';
269 self
.rightZoomHandle_
.style
.left
= (rightHandlePos
- halfHandleWidth
) + 'px';
270 self
.drawInteractiveLayer_();
272 // Do pan on the fly (if not using excanvas).
273 if (!self
.isUsingExcanvas
) {
278 function onPanEnd(e
) {
283 Dygraph
.removeEvent(topElem
, 'mousemove', onPan
);
284 Dygraph
.removeEvent(topElem
, 'mouseup', onPanEnd
);
285 // If using excanvas, do pan now.
286 if (self
.isUsingExcanvas
) {
293 self
.isChangingRange_
= true;
294 self
.dygraph_
.dateWindow_
= toXDataWindow(self
.getZoomHandleStatus_());
295 self
.dygraph_
.drawGraph_(false);
297 self
.isChangingRange_
= false;
301 function onCanvasMouseMove(e
) {
302 if (isZooming
|| isPanning
) {
305 var cursor
= isMouseInPanZone(e
) ? 'move' : 'default';
306 if (cursor
!= self
.fgcanvas_
.style
.cursor
) {
307 self
.fgcanvas_
.style
.cursor
= cursor
;
311 var interactionModel
= {
312 mousedown
: function(event
, g
, context
) {
313 context
.initializeMouseDown(event
, g
, context
);
314 Dygraph
.startPan(event
, g
, context
);
316 mousemove
: function(event
, g
, context
) {
317 if (context
.isPanning
) {
318 Dygraph
.movePan(event
, g
, context
);
321 mouseup
: function(event
, g
, context
) {
322 if (context
.isPanning
) {
323 Dygraph
.endPan(event
, g
, context
);
328 this.dygraph_
.attrs_
.interactionModel
= interactionModel
;
329 this.dygraph_
.attrs_
.panEdgeFraction
= .0001;
331 Dygraph
.addEvent(this.leftZoomHandle_
, 'dragstart', onZoomStart
);
332 Dygraph
.addEvent(this.rightZoomHandle_
, 'dragstart', onZoomStart
);
333 Dygraph
.addEvent(this.fgcanvas_
, 'mousedown', onPanStart
);
334 Dygraph
.addEvent(this.fgcanvas_
, 'mousemove', onCanvasMouseMove
);
339 * Draws the static layer in the background canvas.
341 DygraphRangeSelector
.prototype.drawStaticLayer_
= function() {
342 var ctx
= this.bgcanvas_ctx_
;
343 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
346 this.drawMiniPlot_();
349 ctx
.strokeStyle
= 'lightgray';
351 ctx
.strokeRect(margin
, margin
, this.canvasRect_
.w
-margin
, this.canvasRect_
.h
-margin
);
354 ctx
.moveTo(margin
, margin
);
355 ctx
.lineTo(margin
, this.canvasRect_
.h
-margin
);
356 ctx
.lineTo(this.canvasRect_
.w
-margin
, this.canvasRect_
.h
-margin
);
357 ctx
.lineTo(this.canvasRect_
.w
-margin
, margin
);
365 * Draws the mini plot in the background canvas.
367 DygraphRangeSelector
.prototype.drawMiniPlot_
= function() {
368 var fillStyle
= this.attr_('rangeSelectorPlotFillColor');
369 var strokeStyle
= this.attr_('rangeSelectorPlotStrokeColor');
370 if (!fillStyle
&& !strokeStyle
) {
374 var combinedSeriesData
= this.computeCombinedSeriesAndLimits_();
375 var yRange
= combinedSeriesData
.yMax
- combinedSeriesData
.yMin
;
377 // Draw the mini plot.
378 var ctx
= this.bgcanvas_ctx_
;
381 var xExtremes
= this.dygraph_
.xAxisExtremes();
382 var xRange
= Math
.max(xExtremes
[1] - xExtremes
[0], 1.e
-30);
383 var xFact
= (this.canvasRect_
.w
- margin
)/xRange
;
384 var yFact
= (this.canvasRect_
.h
- margin
)/yRange
;
385 var canvasWidth
= this.canvasRect_
.w
- margin
;
386 var canvasHeight
= this.canvasRect_
.h
- margin
;
389 ctx
.moveTo(margin
, canvasHeight
);
390 for (var i
= 0; i
< combinedSeriesData
.data
.length
; i
++) {
391 var dataPoint
= combinedSeriesData
.data
[i
];
392 var x
= (dataPoint
[0] - xExtremes
[0])*xFact
;
393 var y
= canvasHeight
- (dataPoint
[1] - combinedSeriesData
.yMin
)*yFact
;
394 if (isFinite(x
) && isFinite(y
)) {
398 ctx
.lineTo(canvasWidth
, canvasHeight
);
402 var lingrad
= this.bgcanvas_ctx_
.createLinearGradient(0, 0, 0, canvasHeight
);
403 lingrad
.addColorStop(0, 'white');
404 lingrad
.addColorStop(1, fillStyle
);
405 this.bgcanvas_ctx_
.fillStyle
= lingrad
;
410 this.bgcanvas_ctx_
.strokeStyle
= strokeStyle
;
411 this.bgcanvas_ctx_
.lineWidth
= 1.5;
418 * Computes and returns the combinded series data along with min/max for the mini plot.
419 * @return {Object} An object containing combinded series array, ymin, ymax.
421 DygraphRangeSelector
.prototype.computeCombinedSeriesAndLimits_
= function() {
422 var data
= this.dygraph_
.rawData_
;
423 var logscale
= this.attr_('logscale');
425 // Create a combined series (average of all series values).
426 var combinedSeries
= [];
429 var mutipleValues
= typeof data
[0][1] != 'number';
434 for (var k
= 0; k
< data
[0][1].length
; k
++) {
438 mutipleValues
= true;
441 for (var i
= 0; i
< data
.length
; i
++) {
442 var dataPoint
= data
[i
];
443 var xVal
= dataPoint
[0];
447 for (var k
= 0; k
< sum
.length
; k
++) {
448 sum
[k
] = count
[k
] = 0;
454 for (var j
= 1; j
< dataPoint
.length
; j
++) {
455 if (this.dygraph_
.visibility()[j
-1]) {
457 for (var k
= 0; k
< sum
.length
; k
++) {
458 var y
= dataPoint
[j
][k
];
459 if (y
== null || isNaN(y
)) continue;
464 var y
= dataPoint
[j
];
465 if (y
== null || isNaN(y
)) continue;
473 for (var k
= 0; k
< sum
.length
; k
++) {
481 combinedSeries
.push([xVal
, yVal
]);
484 // Account for roll period, fractions.
485 combinedSeries
= this.dygraph_
.rollingAverage(combinedSeries
, this.dygraph_
.rollPeriod_
);
487 if (typeof combinedSeries
[0][1] != 'number') {
488 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
489 var yVal
= combinedSeries
[i
][1];
490 combinedSeries
[i
][1] = yVal
[0];
494 // Compute the y range.
495 var yMin
= Number
.MAX_VALUE
;
496 var yMax
= -Number
.MAX_VALUE
;
497 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
498 var yVal
= combinedSeries
[i
][1];
499 if (!logscale
|| yVal
> 0) {
500 yMin
= Math
.min(yMin
, yVal
);
501 yMax
= Math
.max(yMax
, yVal
);
505 // Convert Y data to log scale if needed.
506 // Also, expand the Y range to compress the mini plot a little.
507 var extraPercent
= .25;
509 yMax
= Dygraph
.log10(yMax
);
510 yMax
+= yMax
*extraPercent
;
511 yMin
= Dygraph
.log10(yMin
);
512 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
513 combinedSeries
[i
][1] = Dygraph
.log10(combinedSeries
[i
][1]);
517 yRange
= yMax
- yMin
;
518 if (yRange
<= Number
.MIN_VALUE
) {
519 yExtra
= yMax
*extraPercent
;
521 yExtra
= yRange
*extraPercent
;
527 return {data
: combinedSeries
, yMin
: yMin
, yMax
: yMax
};
532 * Places the zoom handles in the proper position based on the current X data window.
534 DygraphRangeSelector
.prototype.placeZoomHandles_
= function() {
535 var xExtremes
= this.dygraph_
.xAxisExtremes();
536 var xWindowLimits
= this.dygraph_
.xAxisRange();
537 var xRange
= xExtremes
[1] - xExtremes
[0];
538 var leftPercent
= Math
.max(0, (xWindowLimits
[0] - xExtremes
[0])/xRange
);
539 var rightPercent
= Math
.max(0, (xExtremes
[1] - xWindowLimits
[1])/xRange
);
540 var leftCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*leftPercent
;
541 var rightCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*(1 - rightPercent
);
542 var handleTop
= Math
.round(Math
.max(this.canvasRect_
.y
, this.canvasRect_
.y
+ (this.canvasRect_
.h
- this.leftZoomHandle_
.height
)/2));
543 var halfHandleWidth
= Math
.round(this.leftZoomHandle_
.width
/2);
544 this.leftZoomHandle_
.style
.left
= Math
.round(leftCoord
- halfHandleWidth
) + 'px';
545 this.leftZoomHandle_
.style
.top
= handleTop
+ 'px';
546 this.rightZoomHandle_
.style
.left
= Math
.round(rightCoord
- halfHandleWidth
) + 'px';
547 this.rightZoomHandle_
.style
.top
= this.leftZoomHandle_
.style
.top
;
549 this.leftZoomHandle_
.style
.visibility
= 'visible';
550 this.rightZoomHandle_
.style
.visibility
= 'visible';
555 * Draws the interactive layer in the foreground canvas.
557 DygraphRangeSelector
.prototype.drawInteractiveLayer_
= function() {
558 var ctx
= this.fgcanvas_ctx_
;
559 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
561 var width
= this.canvasRect_
.w
- margin
;
562 var height
= this.canvasRect_
.h
- margin
;
563 var zoomHandleStatus
= this.getZoomHandleStatus_();
565 ctx
.strokeStyle
= 'black';
566 if (!zoomHandleStatus
.isZoomed
) {
568 ctx
.moveTo(margin
, margin
);
569 ctx
.lineTo(margin
, height
);
570 ctx
.lineTo(width
, height
);
571 ctx
.lineTo(width
, margin
);
574 leftHandleCanvasPos
= Math
.max(margin
, zoomHandleStatus
.leftHandlePos
- this.canvasRect_
.x
);
575 rightHandleCanvasPos
= Math
.min(width
, zoomHandleStatus
.rightHandlePos
- this.canvasRect_
.x
);
577 ctx
.fillStyle
= 'rgba(240, 240, 240, 0.6)';
578 ctx
.fillRect(margin
, margin
, leftHandleCanvasPos
, height
- margin
);
579 ctx
.fillRect(rightHandleCanvasPos
, margin
, width
- rightHandleCanvasPos
, height
- margin
);
582 ctx
.moveTo(margin
, margin
);
583 ctx
.lineTo(leftHandleCanvasPos
, margin
);
584 ctx
.lineTo(leftHandleCanvasPos
, height
);
585 ctx
.lineTo(rightHandleCanvasPos
, height
);
586 ctx
.lineTo(rightHandleCanvasPos
, margin
);
587 ctx
.lineTo(width
, margin
);
594 * Returns the current zoom handle position information.
595 * @return {Object} The zoom handle status.
597 DygraphRangeSelector
.prototype.getZoomHandleStatus_
= function() {
598 var halfHandleWidth
= Math
.round(this.leftZoomHandle_
.width
/2);
599 var leftHandlePos
= parseInt(this.leftZoomHandle_
.style
.left
) + halfHandleWidth
;
600 var rightHandlePos
= parseInt(this.rightZoomHandle_
.style
.left
) + halfHandleWidth
;
602 leftHandlePos
: leftHandlePos
,
603 rightHandlePos
: rightHandlePos
,
604 isZoomed
: (leftHandlePos
- 1 > this.canvasRect_
.x
|| rightHandlePos
+ 1 < this.canvasRect_
.x
+this.canvasRect_
.w
)