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_
= dygraph
.isUsingExcanvas_
;
17 this.dygraph_
= dygraph
;
18 this.createCanvases_();
19 if (this.isUsingExcanvas_
) {
20 this.createIEPanOverlay_();
22 this.createZoomHandles_();
23 this.initInteraction_();
27 * Adds the range selector to the dygraph.
28 * @param {Object} graphDiv The container div for the range selector.
29 * @param {DygraphLayout} layout The DygraphLayout object for this graph.
31 DygraphRangeSelector
.prototype.addToGraph
= function(graphDiv
, layout
) {
32 this.layout_
= layout
;
34 graphDiv
.appendChild(this.bgcanvas_
);
35 graphDiv
.appendChild(this.fgcanvas_
);
36 graphDiv
.appendChild(this.leftZoomHandle_
);
37 graphDiv
.appendChild(this.rightZoomHandle_
);
41 * Renders the static background portion of the range selector.
43 DygraphRangeSelector
.prototype.renderStaticLayer
= function() {
45 this.drawStaticLayer_();
49 * Renders the interactive foreground portion of the range selector.
51 DygraphRangeSelector
.prototype.renderInteractiveLayer
= function() {
52 if (this.isChangingRange_
) {
55 this.placeZoomHandles_();
56 this.drawInteractiveLayer_();
61 * Resizes the range selector.
63 DygraphRangeSelector
.prototype.resize_
= function() {
64 function setElementRect(canvas
, rect
) {
65 canvas
.style
.top
= rect
.y
+ 'px';
66 canvas
.style
.left
= rect
.x
+ 'px';
67 canvas
.width
= rect
.w
;
68 canvas
.height
= rect
.h
;
69 canvas
.style
.width
= canvas
.width
+ 'px'; // for IE
70 canvas
.style
.height
= canvas
.height
+ 'px'; // for IE
73 var plotArea
= this.layout_
.getPlotArea();
74 var xAxisLabelHeight
= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
77 y
: plotArea
.y
+ plotArea
.h
+ xAxisLabelHeight
+ 4,
79 h
: this.attr_('rangeSelectorHeight')
82 setElementRect(this.bgcanvas_
, this.canvasRect_
);
83 setElementRect(this.fgcanvas_
, this.canvasRect_
);
86 DygraphRangeSelector
.prototype.attr_
= function(name
) {
87 return this.dygraph_
.attr_(name
);
92 * Creates the background and foreground canvases.
94 DygraphRangeSelector
.prototype.createCanvases_
= function() {
95 this.bgcanvas_
= Dygraph
.createCanvas();
96 this.bgcanvas_
.className
= 'dygraph-rangesel-bgcanvas';
97 this.bgcanvas_
.style
.position
= 'absolute';
98 this.bgcanvas_
.style
.zIndex
= 9;
99 this.bgcanvas_ctx_
= Dygraph
.getContext(this.bgcanvas_
);
101 this.fgcanvas_
= Dygraph
.createCanvas();
102 this.fgcanvas_
.className
= 'dygraph-rangesel-fgcanvas';
103 this.fgcanvas_
.style
.position
= 'absolute';
104 this.fgcanvas_
.style
.zIndex
= 9;
105 this.fgcanvas_
.style
.cursor
= 'default';
106 this.fgcanvas_ctx_
= Dygraph
.getContext(this.fgcanvas_
);
111 * Creates overlay divs for IE/Excanvas so that mouse events are handled properly.
113 DygraphRangeSelector
.prototype.createIEPanOverlay_
= function() {
114 this.iePanOverlay_
= document
.createElement("div");
115 this.iePanOverlay_
.style
.position
= 'absolute';
116 this.iePanOverlay_
.style
.backgroundColor
= 'white';
117 this.iePanOverlay_
.style
.filter
= 'alpha(opacity=0)';
118 this.iePanOverlay_
.style
.display
= 'none';
119 this.iePanOverlay_
.style
.cursor
= 'move';
120 this.fgcanvas_
.appendChild(this.iePanOverlay_
);
125 * Creates the zoom handle elements.
127 DygraphRangeSelector
.prototype.createZoomHandles_
= function() {
128 var img
= new Image();
129 img
.className
= 'dygraph-rangesel-zoomhandle';
130 img
.style
.position
= 'absolute';
131 img
.style
.zIndex
= 10;
132 img
.style
.visibility
= 'hidden'; // Initially hidden so they don't show up in the wrong place.
133 img
.style
.cursor
= 'col-resize';
134 if (/MSIE 7/.test(navigator
.userAgent
)) { // IE7 doesn't support embedded src data.
137 img
.style
.backgroundColor
= 'white';
138 img
.style
.border
= '1px solid #333333'; // Just show box in IE7.
142 img
.src
= 'data:image/png;base64,\
143 iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
144 zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
145 bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
146 6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
147 qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
150 this.leftZoomHandle_
= img
;
151 this.rightZoomHandle_
= img
.cloneNode(false);
156 * Sets up the interaction for the range selector.
158 DygraphRangeSelector
.prototype.initInteraction_
= function() {
160 var topElem
= this.isIE_
? document
: window
;
163 var isZooming
= false;
164 var isPanning
= false;
166 function toXDataWindow(zoomHandleStatus
) {
167 var xDataLimits
= self
.dygraph_
.xAxisExtremes();
168 var fact
= (xDataLimits
[1] - xDataLimits
[0])/self
.canvasRect_
.w
;
169 var xDataMin
= xDataLimits
[0] + (zoomHandleStatus
.leftHandlePos
- self
.canvasRect_
.x
)*fact
;
170 var xDataMax
= xDataLimits
[0] + (zoomHandleStatus
.rightHandlePos
- self
.canvasRect_
.x
)*fact
;
171 return [xDataMin
, xDataMax
];
174 function onZoomStart(e
) {
175 Dygraph
.cancelEvent(e
);
178 handle
= e
.target
? e
.target
: e
.srcElement
;
179 Dygraph
.addEvent(topElem
, 'mousemove', onZoom
);
180 Dygraph
.addEvent(topElem
, 'mouseup', onZoomEnd
);
181 self
.fgcanvas_
.style
.cursor
= 'col-resize';
188 var delX
= e
.screenX
- xLast
;
189 if (Math
.abs(delX
) < 4) {
193 var zoomHandleStatus
= self
.getZoomHandleStatus_();
194 if (handle
== self
.leftZoomHandle_
) {
195 var newPos
= zoomHandleStatus
.leftHandlePos
+ delX
;
196 newPos
= Math
.min(newPos
, zoomHandleStatus
.rightHandlePos
- handle
.width
- 3);
197 newPos
= Math
.max(newPos
, self
.canvasRect_
.x
);
199 var newPos
= zoomHandleStatus
.rightHandlePos
+ delX
;
200 newPos
= Math
.min(newPos
, self
.canvasRect_
.x
+ self
.canvasRect_
.w
);
201 newPos
= Math
.max(newPos
, zoomHandleStatus
.leftHandlePos
+ handle
.width
+ 3);
203 var halfHandleWidth
= handle
.width
/2;
204 handle
.style
.left
= (newPos
- halfHandleWidth
) + 'px';
205 self
.drawInteractiveLayer_();
207 // Zoom on the fly (if not using excanvas).
208 if (!self
.isUsingExcanvas_
) {
213 function onZoomEnd(e
) {
218 Dygraph
.removeEvent(topElem
, 'mousemove', onZoom
);
219 Dygraph
.removeEvent(topElem
, 'mouseup', onZoomEnd
);
220 self
.fgcanvas_
.style
.cursor
= 'default';
222 // If using excanvas, Zoom now.
223 if (self
.isUsingExcanvas_
) {
230 var zoomHandleStatus
= self
.getZoomHandleStatus_();
231 self
.isChangingRange_
= true;
232 if (!zoomHandleStatus
.isZoomed
) {
233 self
.dygraph_
.doUnzoom_();
235 var xDataWindow
= toXDataWindow(zoomHandleStatus
);
236 self
.dygraph_
.doZoomXDates_(xDataWindow
[0], xDataWindow
[1]);
239 self
.isChangingRange_
= false;
243 function isMouseInPanZone(e
) {
244 if (self
.isUsingExcanvas_
) {
245 return e
.srcElement
== self
.iePanOverlay_
;
247 // Getting clientX directly from the event is not accurate enough :(
248 var clientX
= self
.canvasRect_
.x
+ (e
.layerX
!= undefined
? e
.layerX
: e
.offsetX
);
249 var zoomHandleStatus
= self
.getZoomHandleStatus_();
250 return (clientX
> zoomHandleStatus
.leftHandlePos
&& clientX
< zoomHandleStatus
.rightHandlePos
);
254 function onPanStart(e
) {
255 if (!isPanning
&& isMouseInPanZone(e
) && self
.getZoomHandleStatus_().isZoomed
) {
256 Dygraph
.cancelEvent(e
);
259 Dygraph
.addEvent(topElem
, 'mousemove', onPan
);
260 Dygraph
.addEvent(topElem
, 'mouseup', onPanEnd
);
268 Dygraph
.cancelEvent(e
);
270 var delX
= e
.screenX
- xLast
;
271 if (Math
.abs(delX
) < 4) {
277 var zoomHandleStatus
= self
.getZoomHandleStatus_();
278 var leftHandlePos
= zoomHandleStatus
.leftHandlePos
;
279 var rightHandlePos
= zoomHandleStatus
.rightHandlePos
;
280 var rangeSize
= rightHandlePos
- leftHandlePos
;
281 if (leftHandlePos
+ delX
<= self
.canvasRect_
.x
) {
282 leftHandlePos
= self
.canvasRect_
.x
;
283 rightHandlePos
= leftHandlePos
+ rangeSize
;
284 } else if (rightHandlePos
+ delX
>= self
.canvasRect_
.x
+ self
.canvasRect_
.w
) {
285 rightHandlePos
= self
.canvasRect_
.x
+ self
.canvasRect_
.w
;
286 leftHandlePos
= rightHandlePos
- rangeSize
;
288 leftHandlePos
+= delX
;
289 rightHandlePos
+= delX
;
291 var halfHandleWidth
= self
.leftZoomHandle_
.width
/2;
292 self
.leftZoomHandle_
.style
.left
= (leftHandlePos
- halfHandleWidth
) + 'px';
293 self
.rightZoomHandle_
.style
.left
= (rightHandlePos
- halfHandleWidth
) + 'px';
294 self
.drawInteractiveLayer_();
296 // Do pan on the fly (if not using excanvas).
297 if (!self
.isUsingExcanvas_
) {
302 function onPanEnd(e
) {
307 Dygraph
.removeEvent(topElem
, 'mousemove', onPan
);
308 Dygraph
.removeEvent(topElem
, 'mouseup', onPanEnd
);
309 // If using excanvas, do pan now.
310 if (self
.isUsingExcanvas_
) {
317 self
.isChangingRange_
= true;
318 self
.dygraph_
.dateWindow_
= toXDataWindow(self
.getZoomHandleStatus_());
319 self
.dygraph_
.drawGraph_(false);
321 self
.isChangingRange_
= false;
325 function onCanvasMouseMove(e
) {
326 if (isZooming
|| isPanning
) {
329 var cursor
= isMouseInPanZone(e
) ? 'move' : 'default';
330 if (cursor
!= self
.fgcanvas_
.style
.cursor
) {
331 self
.fgcanvas_
.style
.cursor
= cursor
;
335 var interactionModel
= {
336 mousedown
: function(event
, g
, context
) {
337 context
.initializeMouseDown(event
, g
, context
);
338 Dygraph
.startPan(event
, g
, context
);
340 mousemove
: function(event
, g
, context
) {
341 if (context
.isPanning
) {
342 Dygraph
.movePan(event
, g
, context
);
345 mouseup
: function(event
, g
, context
) {
346 if (context
.isPanning
) {
347 Dygraph
.endPan(event
, g
, context
);
352 this.dygraph_
.attrs_
.interactionModel
= interactionModel
;
353 this.dygraph_
.attrs_
.panEdgeFraction
= .0001;
355 var dragStartEvent
= window
.opera
? 'mousedown' : 'dragstart';
356 Dygraph
.addEvent(this.leftZoomHandle_
, dragStartEvent
, onZoomStart
);
357 Dygraph
.addEvent(this.rightZoomHandle_
, dragStartEvent
, onZoomStart
);
359 if (this.isUsingExcanvas_
) {
360 Dygraph
.addEvent(this.iePanOverlay_
, 'mousedown', onPanStart
);
362 Dygraph
.addEvent(this.fgcanvas_
, 'mousedown', onPanStart
);
363 Dygraph
.addEvent(this.fgcanvas_
, 'mousemove', onCanvasMouseMove
);
369 * Draws the static layer in the background canvas.
371 DygraphRangeSelector
.prototype.drawStaticLayer_
= function() {
372 var ctx
= this.bgcanvas_ctx_
;
373 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
375 this.drawMiniPlot_();
380 this.bgcanvas_ctx_
.lineWidth
= 1;
381 ctx
.strokeStyle
= 'gray';
383 ctx
.moveTo(margin
, margin
);
384 ctx
.lineTo(margin
, this.canvasRect_
.h
-margin
);
385 ctx
.lineTo(this.canvasRect_
.w
-margin
, this.canvasRect_
.h
-margin
);
386 ctx
.lineTo(this.canvasRect_
.w
-margin
, margin
);
393 * Draws the mini plot in the background canvas.
395 DygraphRangeSelector
.prototype.drawMiniPlot_
= function() {
396 var fillStyle
= this.attr_('rangeSelectorPlotFillColor');
397 var strokeStyle
= this.attr_('rangeSelectorPlotStrokeColor');
398 if (!fillStyle
&& !strokeStyle
) {
402 var combinedSeriesData
= this.computeCombinedSeriesAndLimits_();
403 var yRange
= combinedSeriesData
.yMax
- combinedSeriesData
.yMin
;
405 // Draw the mini plot.
406 var ctx
= this.bgcanvas_ctx_
;
409 var xExtremes
= this.dygraph_
.xAxisExtremes();
410 var xRange
= Math
.max(xExtremes
[1] - xExtremes
[0], 1.e
-30);
411 var xFact
= (this.canvasRect_
.w
- margin
)/xRange
;
412 var yFact
= (this.canvasRect_
.h
- margin
)/yRange
;
413 var canvasWidth
= this.canvasRect_
.w
- margin
;
414 var canvasHeight
= this.canvasRect_
.h
- margin
;
417 ctx
.moveTo(margin
, canvasHeight
);
418 for (var i
= 0; i
< combinedSeriesData
.data
.length
; i
++) {
419 var dataPoint
= combinedSeriesData
.data
[i
];
420 var x
= (dataPoint
[0] - xExtremes
[0])*xFact
;
421 var y
= canvasHeight
- (dataPoint
[1] - combinedSeriesData
.yMin
)*yFact
;
422 if (isFinite(x
) && isFinite(y
)) {
426 ctx
.lineTo(canvasWidth
, canvasHeight
);
430 var lingrad
= this.bgcanvas_ctx_
.createLinearGradient(0, 0, 0, canvasHeight
);
431 lingrad
.addColorStop(0, 'white');
432 lingrad
.addColorStop(1, fillStyle
);
433 this.bgcanvas_ctx_
.fillStyle
= lingrad
;
438 this.bgcanvas_ctx_
.strokeStyle
= strokeStyle
;
439 this.bgcanvas_ctx_
.lineWidth
= 1.5;
446 * Computes and returns the combinded series data along with min/max for the mini plot.
447 * @return {Object} An object containing combinded series array, ymin, ymax.
449 DygraphRangeSelector
.prototype.computeCombinedSeriesAndLimits_
= function() {
450 var data
= this.dygraph_
.rawData_
;
451 var logscale
= this.attr_('logscale');
453 // Create a combined series (average of all series values).
454 var combinedSeries
= [];
457 var mutipleValues
= typeof data
[0][1] != 'number';
462 for (var k
= 0; k
< data
[0][1].length
; k
++) {
466 mutipleValues
= true;
469 for (var i
= 0; i
< data
.length
; i
++) {
470 var dataPoint
= data
[i
];
471 var xVal
= dataPoint
[0];
475 for (var k
= 0; k
< sum
.length
; k
++) {
476 sum
[k
] = count
[k
] = 0;
482 for (var j
= 1; j
< dataPoint
.length
; j
++) {
483 if (this.dygraph_
.visibility()[j
-1]) {
485 for (var k
= 0; k
< sum
.length
; k
++) {
486 var y
= dataPoint
[j
][k
];
487 if (y
== null || isNaN(y
)) continue;
492 var y
= dataPoint
[j
];
493 if (y
== null || isNaN(y
)) continue;
501 for (var k
= 0; k
< sum
.length
; k
++) {
509 combinedSeries
.push([xVal
, yVal
]);
512 // Account for roll period, fractions.
513 combinedSeries
= this.dygraph_
.rollingAverage(combinedSeries
, this.dygraph_
.rollPeriod_
);
515 if (typeof combinedSeries
[0][1] != 'number') {
516 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
517 var yVal
= combinedSeries
[i
][1];
518 combinedSeries
[i
][1] = yVal
[0];
522 // Compute the y range.
523 var yMin
= Number
.MAX_VALUE
;
524 var yMax
= -Number
.MAX_VALUE
;
525 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
526 var yVal
= combinedSeries
[i
][1];
527 if (yVal
!= null && isFinite(yVal
) && (!logscale
|| yVal
> 0)) {
528 yMin
= Math
.min(yMin
, yVal
);
529 yMax
= Math
.max(yMax
, yVal
);
533 // Convert Y data to log scale if needed.
534 // Also, expand the Y range to compress the mini plot a little.
535 var extraPercent
= .25;
537 yMax
= Dygraph
.log10(yMax
);
538 yMax
+= yMax
*extraPercent
;
539 yMin
= Dygraph
.log10(yMin
);
540 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
541 combinedSeries
[i
][1] = Dygraph
.log10(combinedSeries
[i
][1]);
545 yRange
= yMax
- yMin
;
546 if (yRange
<= Number
.MIN_VALUE
) {
547 yExtra
= yMax
*extraPercent
;
549 yExtra
= yRange
*extraPercent
;
555 return {data
: combinedSeries
, yMin
: yMin
, yMax
: yMax
};
560 * Places the zoom handles in the proper position based on the current X data window.
562 DygraphRangeSelector
.prototype.placeZoomHandles_
= function() {
563 var xExtremes
= this.dygraph_
.xAxisExtremes();
564 var xWindowLimits
= this.dygraph_
.xAxisRange();
565 var xRange
= xExtremes
[1] - xExtremes
[0];
566 var leftPercent
= Math
.max(0, (xWindowLimits
[0] - xExtremes
[0])/xRange
);
567 var rightPercent
= Math
.max(0, (xExtremes
[1] - xWindowLimits
[1])/xRange
);
568 var leftCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*leftPercent
;
569 var rightCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*(1 - rightPercent
);
570 var handleTop
= Math
.max(this.canvasRect_
.y
, this.canvasRect_
.y
+ (this.canvasRect_
.h
- this.leftZoomHandle_
.height
)/2);
571 var halfHandleWidth
= this.leftZoomHandle_
.width
/2;
572 this.leftZoomHandle_
.style
.left
= (leftCoord
- halfHandleWidth
) + 'px';
573 this.leftZoomHandle_
.style
.top
= handleTop
+ 'px';
574 this.rightZoomHandle_
.style
.left
= (rightCoord
- halfHandleWidth
) + 'px';
575 this.rightZoomHandle_
.style
.top
= this.leftZoomHandle_
.style
.top
;
577 this.leftZoomHandle_
.style
.visibility
= 'visible';
578 this.rightZoomHandle_
.style
.visibility
= 'visible';
583 * Draws the interactive layer in the foreground canvas.
585 DygraphRangeSelector
.prototype.drawInteractiveLayer_
= function() {
586 var ctx
= this.fgcanvas_ctx_
;
587 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
589 var width
= this.canvasRect_
.w
- margin
;
590 var height
= this.canvasRect_
.h
- margin
;
591 var zoomHandleStatus
= this.getZoomHandleStatus_();
593 ctx
.strokeStyle
= 'black';
594 if (!zoomHandleStatus
.isZoomed
) {
596 ctx
.moveTo(margin
, margin
);
597 ctx
.lineTo(margin
, height
);
598 ctx
.lineTo(width
, height
);
599 ctx
.lineTo(width
, margin
);
601 if (this.iePanOverlay_
) {
602 this.iePanOverlay_
.style
.display
= 'none';
605 leftHandleCanvasPos
= Math
.max(margin
, zoomHandleStatus
.leftHandlePos
- this.canvasRect_
.x
);
606 rightHandleCanvasPos
= Math
.min(width
, zoomHandleStatus
.rightHandlePos
- this.canvasRect_
.x
);
608 ctx
.fillStyle
= 'rgba(240, 240, 240, 0.6)';
609 ctx
.fillRect(0, 0, leftHandleCanvasPos
, this.canvasRect_
.h
);
610 ctx
.fillRect(rightHandleCanvasPos
, 0, this.canvasRect_
.w
- rightHandleCanvasPos
, this.canvasRect_
.h
);
613 ctx
.moveTo(margin
, margin
);
614 ctx
.lineTo(leftHandleCanvasPos
, margin
);
615 ctx
.lineTo(leftHandleCanvasPos
, height
);
616 ctx
.lineTo(rightHandleCanvasPos
, height
);
617 ctx
.lineTo(rightHandleCanvasPos
, margin
);
618 ctx
.lineTo(width
, margin
);
621 if (this.isUsingExcanvas_
) {
622 this.iePanOverlay_
.style
.width
= (rightHandleCanvasPos
- leftHandleCanvasPos
) + 'px';
623 this.iePanOverlay_
.style
.left
= leftHandleCanvasPos
+ 'px';
624 this.iePanOverlay_
.style
.height
= height
+ 'px';
625 this.iePanOverlay_
.style
.display
= 'inline';
632 * Returns the current zoom handle position information.
633 * @return {Object} The zoom handle status.
635 DygraphRangeSelector
.prototype.getZoomHandleStatus_
= function() {
636 var halfHandleWidth
= this.leftZoomHandle_
.width
/2;
637 var leftHandlePos
= parseInt(this.leftZoomHandle_
.style
.left
) + halfHandleWidth
;
638 var rightHandlePos
= parseInt(this.rightZoomHandle_
.style
.left
) + halfHandleWidth
;
640 leftHandlePos
: leftHandlePos
,
641 rightHandlePos
: rightHandlePos
,
642 isZoomed
: (leftHandlePos
- 1 > this.canvasRect_
.x
|| rightHandlePos
+ 1 < this.canvasRect_
.x
+this.canvasRect_
.w
)