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.
12 * The DygraphRangeSelector class provides a timeline range selector widget.
13 * @param {Dygraph} dygraph The dygraph object
16 var DygraphRangeSelector
= function(dygraph
) {
17 this.isIE_
= /MSIE/.test(navigator
.userAgent
) && !window
.opera
;
18 this.isUsingExcanvas_
= dygraph
.isUsingExcanvas_
;
19 this.dygraph_
= dygraph
;
20 this.createCanvases_();
21 if (this.isUsingExcanvas_
) {
22 this.createIEPanOverlay_();
24 this.createZoomHandles_();
25 this.initInteraction_();
29 * Adds the range selector to the dygraph.
30 * @param {Object} graphDiv The container div for the range selector.
31 * @param {DygraphLayout} layout The DygraphLayout object for this graph.
33 DygraphRangeSelector
.prototype.addToGraph
= function(graphDiv
, layout
) {
34 this.layout_
= layout
;
36 graphDiv
.appendChild(this.bgcanvas_
);
37 graphDiv
.appendChild(this.fgcanvas_
);
38 graphDiv
.appendChild(this.leftZoomHandle_
);
39 graphDiv
.appendChild(this.rightZoomHandle_
);
43 * Renders the static background portion of the range selector.
45 DygraphRangeSelector
.prototype.renderStaticLayer
= function() {
47 this.drawStaticLayer_();
51 * Renders the interactive foreground portion of the range selector.
53 DygraphRangeSelector
.prototype.renderInteractiveLayer
= function() {
54 if (this.isChangingRange_
) {
57 this.placeZoomHandles_();
58 this.drawInteractiveLayer_();
63 * Resizes the range selector.
65 DygraphRangeSelector
.prototype.resize_
= function() {
66 function setElementRect(canvas
, rect
) {
67 canvas
.style
.top
= rect
.y
+ 'px';
68 canvas
.style
.left
= rect
.x
+ 'px';
69 canvas
.width
= rect
.w
;
70 canvas
.height
= rect
.h
;
71 canvas
.style
.width
= canvas
.width
+ 'px'; // for IE
72 canvas
.style
.height
= canvas
.height
+ 'px'; // for IE
75 var plotArea
= this.layout_
.getPlotArea();
76 var xAxisLabelHeight
= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
79 y
: plotArea
.y
+ plotArea
.h
+ xAxisLabelHeight
+ 4,
81 h
: this.attr_('rangeSelectorHeight')
84 setElementRect(this.bgcanvas_
, this.canvasRect_
);
85 setElementRect(this.fgcanvas_
, this.canvasRect_
);
88 DygraphRangeSelector
.prototype.attr_
= function(name
) {
89 return this.dygraph_
.attr_(name
);
94 * Creates the background and foreground canvases.
96 DygraphRangeSelector
.prototype.createCanvases_
= function() {
97 this.bgcanvas_
= Dygraph
.createCanvas();
98 this.bgcanvas_
.className
= 'dygraph-rangesel-bgcanvas';
99 this.bgcanvas_
.style
.position
= 'absolute';
100 this.bgcanvas_
.style
.zIndex
= 9;
101 this.bgcanvas_ctx_
= Dygraph
.getContext(this.bgcanvas_
);
103 this.fgcanvas_
= Dygraph
.createCanvas();
104 this.fgcanvas_
.className
= 'dygraph-rangesel-fgcanvas';
105 this.fgcanvas_
.style
.position
= 'absolute';
106 this.fgcanvas_
.style
.zIndex
= 9;
107 this.fgcanvas_
.style
.cursor
= 'default';
108 this.fgcanvas_ctx_
= Dygraph
.getContext(this.fgcanvas_
);
113 * Creates overlay divs for IE/Excanvas so that mouse events are handled properly.
115 DygraphRangeSelector
.prototype.createIEPanOverlay_
= function() {
116 this.iePanOverlay_
= document
.createElement("div");
117 this.iePanOverlay_
.style
.position
= 'absolute';
118 this.iePanOverlay_
.style
.backgroundColor
= 'white';
119 this.iePanOverlay_
.style
.filter
= 'alpha(opacity=0)';
120 this.iePanOverlay_
.style
.display
= 'none';
121 this.iePanOverlay_
.style
.cursor
= 'move';
122 this.fgcanvas_
.appendChild(this.iePanOverlay_
);
127 * Creates the zoom handle elements.
129 DygraphRangeSelector
.prototype.createZoomHandles_
= function() {
130 var img
= new Image();
131 img
.className
= 'dygraph-rangesel-zoomhandle';
132 img
.style
.position
= 'absolute';
133 img
.style
.zIndex
= 10;
134 img
.style
.visibility
= 'hidden'; // Initially hidden so they don't show up in the wrong place.
135 img
.style
.cursor
= 'col-resize';
136 if (/MSIE 7/.test(navigator
.userAgent
)) { // IE7 doesn't support embedded src data.
139 img
.style
.backgroundColor
= 'white';
140 img
.style
.border
= '1px solid #333333'; // Just show box in IE7.
144 img
.src
= 'data:image/png;base64,\
145 iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
146 zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
147 bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
148 6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
149 qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
152 this.leftZoomHandle_
= img
;
153 this.rightZoomHandle_
= img
.cloneNode(false);
158 * Sets up the interaction for the range selector.
160 DygraphRangeSelector
.prototype.initInteraction_
= function() {
162 var topElem
= this.isIE_
? document
: window
;
165 var isZooming
= false;
166 var isPanning
= false;
168 function toXDataWindow(zoomHandleStatus
) {
169 var xDataLimits
= self
.dygraph_
.xAxisExtremes();
170 var fact
= (xDataLimits
[1] - xDataLimits
[0])/self
.canvasRect_
.w
;
171 var xDataMin
= xDataLimits
[0] + (zoomHandleStatus
.leftHandlePos
- self
.canvasRect_
.x
)*fact
;
172 var xDataMax
= xDataLimits
[0] + (zoomHandleStatus
.rightHandlePos
- self
.canvasRect_
.x
)*fact
;
173 return [xDataMin
, xDataMax
];
176 function onZoomStart(e
) {
177 Dygraph
.cancelEvent(e
);
180 handle
= e
.target
? e
.target
: e
.srcElement
;
181 Dygraph
.addEvent(topElem
, 'mousemove', onZoom
);
182 Dygraph
.addEvent(topElem
, 'mouseup', onZoomEnd
);
183 self
.fgcanvas_
.style
.cursor
= 'col-resize';
190 var delX
= e
.screenX
- xLast
;
191 if (Math
.abs(delX
) < 4) {
195 var zoomHandleStatus
= self
.getZoomHandleStatus_();
196 if (handle
== self
.leftZoomHandle_
) {
197 var newPos
= zoomHandleStatus
.leftHandlePos
+ delX
;
198 newPos
= Math
.min(newPos
, zoomHandleStatus
.rightHandlePos
- handle
.width
- 3);
199 newPos
= Math
.max(newPos
, self
.canvasRect_
.x
);
201 var newPos
= zoomHandleStatus
.rightHandlePos
+ delX
;
202 newPos
= Math
.min(newPos
, self
.canvasRect_
.x
+ self
.canvasRect_
.w
);
203 newPos
= Math
.max(newPos
, zoomHandleStatus
.leftHandlePos
+ handle
.width
+ 3);
205 var halfHandleWidth
= handle
.width
/2;
206 handle
.style
.left
= (newPos
- halfHandleWidth
) + 'px';
207 self
.drawInteractiveLayer_();
209 // Zoom on the fly (if not using excanvas).
210 if (!self
.isUsingExcanvas_
) {
215 function onZoomEnd(e
) {
220 Dygraph
.removeEvent(topElem
, 'mousemove', onZoom
);
221 Dygraph
.removeEvent(topElem
, 'mouseup', onZoomEnd
);
222 self
.fgcanvas_
.style
.cursor
= 'default';
224 // If using excanvas, Zoom now.
225 if (self
.isUsingExcanvas_
) {
232 var zoomHandleStatus
= self
.getZoomHandleStatus_();
233 self
.isChangingRange_
= true;
234 if (!zoomHandleStatus
.isZoomed
) {
235 self
.dygraph_
.doUnzoom_();
237 var xDataWindow
= toXDataWindow(zoomHandleStatus
);
238 self
.dygraph_
.doZoomXDates_(xDataWindow
[0], xDataWindow
[1]);
241 self
.isChangingRange_
= false;
245 function isMouseInPanZone(e
) {
246 if (self
.isUsingExcanvas_
) {
247 return e
.srcElement
== self
.iePanOverlay_
;
249 // Getting clientX directly from the event is not accurate enough :(
250 var clientX
= self
.canvasRect_
.x
+ (e
.layerX
!= undefined
? e
.layerX
: e
.offsetX
);
251 var zoomHandleStatus
= self
.getZoomHandleStatus_();
252 return (clientX
> zoomHandleStatus
.leftHandlePos
&& clientX
< zoomHandleStatus
.rightHandlePos
);
256 function onPanStart(e
) {
257 if (!isPanning
&& isMouseInPanZone(e
) && self
.getZoomHandleStatus_().isZoomed
) {
258 Dygraph
.cancelEvent(e
);
261 Dygraph
.addEvent(topElem
, 'mousemove', onPan
);
262 Dygraph
.addEvent(topElem
, 'mouseup', onPanEnd
);
270 Dygraph
.cancelEvent(e
);
272 var delX
= e
.screenX
- xLast
;
273 if (Math
.abs(delX
) < 4) {
279 var zoomHandleStatus
= self
.getZoomHandleStatus_();
280 var leftHandlePos
= zoomHandleStatus
.leftHandlePos
;
281 var rightHandlePos
= zoomHandleStatus
.rightHandlePos
;
282 var rangeSize
= rightHandlePos
- leftHandlePos
;
283 if (leftHandlePos
+ delX
<= self
.canvasRect_
.x
) {
284 leftHandlePos
= self
.canvasRect_
.x
;
285 rightHandlePos
= leftHandlePos
+ rangeSize
;
286 } else if (rightHandlePos
+ delX
>= self
.canvasRect_
.x
+ self
.canvasRect_
.w
) {
287 rightHandlePos
= self
.canvasRect_
.x
+ self
.canvasRect_
.w
;
288 leftHandlePos
= rightHandlePos
- rangeSize
;
290 leftHandlePos
+= delX
;
291 rightHandlePos
+= delX
;
293 var halfHandleWidth
= self
.leftZoomHandle_
.width
/2;
294 self
.leftZoomHandle_
.style
.left
= (leftHandlePos
- halfHandleWidth
) + 'px';
295 self
.rightZoomHandle_
.style
.left
= (rightHandlePos
- halfHandleWidth
) + 'px';
296 self
.drawInteractiveLayer_();
298 // Do pan on the fly (if not using excanvas).
299 if (!self
.isUsingExcanvas_
) {
304 function onPanEnd(e
) {
309 Dygraph
.removeEvent(topElem
, 'mousemove', onPan
);
310 Dygraph
.removeEvent(topElem
, 'mouseup', onPanEnd
);
311 // If using excanvas, do pan now.
312 if (self
.isUsingExcanvas_
) {
319 self
.isChangingRange_
= true;
320 self
.dygraph_
.dateWindow_
= toXDataWindow(self
.getZoomHandleStatus_());
321 self
.dygraph_
.drawGraph_(false);
323 self
.isChangingRange_
= false;
327 function onCanvasMouseMove(e
) {
328 if (isZooming
|| isPanning
) {
331 var cursor
= isMouseInPanZone(e
) ? 'move' : 'default';
332 if (cursor
!= self
.fgcanvas_
.style
.cursor
) {
333 self
.fgcanvas_
.style
.cursor
= cursor
;
337 var interactionModel
= {
338 mousedown
: function(event
, g
, context
) {
339 context
.initializeMouseDown(event
, g
, context
);
340 Dygraph
.startPan(event
, g
, context
);
342 mousemove
: function(event
, g
, context
) {
343 if (context
.isPanning
) {
344 Dygraph
.movePan(event
, g
, context
);
347 mouseup
: function(event
, g
, context
) {
348 if (context
.isPanning
) {
349 Dygraph
.endPan(event
, g
, context
);
354 this.dygraph_
.attrs_
.interactionModel
= interactionModel
;
355 this.dygraph_
.attrs_
.panEdgeFraction
= .0001;
357 var dragStartEvent
= window
.opera
? 'mousedown' : 'dragstart';
358 Dygraph
.addEvent(this.leftZoomHandle_
, dragStartEvent
, onZoomStart
);
359 Dygraph
.addEvent(this.rightZoomHandle_
, dragStartEvent
, onZoomStart
);
361 if (this.isUsingExcanvas_
) {
362 Dygraph
.addEvent(this.iePanOverlay_
, 'mousedown', onPanStart
);
364 Dygraph
.addEvent(this.fgcanvas_
, 'mousedown', onPanStart
);
365 Dygraph
.addEvent(this.fgcanvas_
, 'mousemove', onCanvasMouseMove
);
371 * Draws the static layer in the background canvas.
373 DygraphRangeSelector
.prototype.drawStaticLayer_
= function() {
374 var ctx
= this.bgcanvas_ctx_
;
375 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
377 this.drawMiniPlot_();
382 this.bgcanvas_ctx_
.lineWidth
= 1;
383 ctx
.strokeStyle
= 'gray';
385 ctx
.moveTo(margin
, margin
);
386 ctx
.lineTo(margin
, this.canvasRect_
.h
-margin
);
387 ctx
.lineTo(this.canvasRect_
.w
-margin
, this.canvasRect_
.h
-margin
);
388 ctx
.lineTo(this.canvasRect_
.w
-margin
, margin
);
395 * Draws the mini plot in the background canvas.
397 DygraphRangeSelector
.prototype.drawMiniPlot_
= function() {
398 var fillStyle
= this.attr_('rangeSelectorPlotFillColor');
399 var strokeStyle
= this.attr_('rangeSelectorPlotStrokeColor');
400 if (!fillStyle
&& !strokeStyle
) {
404 var combinedSeriesData
= this.computeCombinedSeriesAndLimits_();
405 var yRange
= combinedSeriesData
.yMax
- combinedSeriesData
.yMin
;
407 // Draw the mini plot.
408 var ctx
= this.bgcanvas_ctx_
;
411 var xExtremes
= this.dygraph_
.xAxisExtremes();
412 var xRange
= Math
.max(xExtremes
[1] - xExtremes
[0], 1.e
-30);
413 var xFact
= (this.canvasRect_
.w
- margin
)/xRange
;
414 var yFact
= (this.canvasRect_
.h
- margin
)/yRange
;
415 var canvasWidth
= this.canvasRect_
.w
- margin
;
416 var canvasHeight
= this.canvasRect_
.h
- margin
;
419 ctx
.moveTo(margin
, canvasHeight
);
420 for (var i
= 0; i
< combinedSeriesData
.data
.length
; i
++) {
421 var dataPoint
= combinedSeriesData
.data
[i
];
422 var x
= (dataPoint
[0] - xExtremes
[0])*xFact
;
423 var y
= canvasHeight
- (dataPoint
[1] - combinedSeriesData
.yMin
)*yFact
;
424 if (isFinite(x
) && isFinite(y
)) {
428 ctx
.lineTo(canvasWidth
, canvasHeight
);
432 var lingrad
= this.bgcanvas_ctx_
.createLinearGradient(0, 0, 0, canvasHeight
);
433 lingrad
.addColorStop(0, 'white');
434 lingrad
.addColorStop(1, fillStyle
);
435 this.bgcanvas_ctx_
.fillStyle
= lingrad
;
440 this.bgcanvas_ctx_
.strokeStyle
= strokeStyle
;
441 this.bgcanvas_ctx_
.lineWidth
= 1.5;
448 * Computes and returns the combinded series data along with min/max for the mini plot.
449 * @return {Object} An object containing combinded series array, ymin, ymax.
451 DygraphRangeSelector
.prototype.computeCombinedSeriesAndLimits_
= function() {
452 var data
= this.dygraph_
.rawData_
;
453 var logscale
= this.attr_('logscale');
455 // Create a combined series (average of all series values).
456 var combinedSeries
= [];
459 var mutipleValues
= typeof data
[0][1] != 'number';
464 for (var k
= 0; k
< data
[0][1].length
; k
++) {
468 mutipleValues
= true;
471 for (var i
= 0; i
< data
.length
; i
++) {
472 var dataPoint
= data
[i
];
473 var xVal
= dataPoint
[0];
477 for (var k
= 0; k
< sum
.length
; k
++) {
478 sum
[k
] = count
[k
] = 0;
484 for (var j
= 1; j
< dataPoint
.length
; j
++) {
485 if (this.dygraph_
.visibility()[j
-1]) {
487 for (var k
= 0; k
< sum
.length
; k
++) {
488 var y
= dataPoint
[j
][k
];
489 if (y
== null || isNaN(y
)) continue;
494 var y
= dataPoint
[j
];
495 if (y
== null || isNaN(y
)) continue;
503 for (var k
= 0; k
< sum
.length
; k
++) {
511 combinedSeries
.push([xVal
, yVal
]);
514 // Account for roll period, fractions.
515 combinedSeries
= this.dygraph_
.rollingAverage(combinedSeries
, this.dygraph_
.rollPeriod_
);
517 if (typeof combinedSeries
[0][1] != 'number') {
518 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
519 var yVal
= combinedSeries
[i
][1];
520 combinedSeries
[i
][1] = yVal
[0];
524 // Compute the y range.
525 var yMin
= Number
.MAX_VALUE
;
526 var yMax
= -Number
.MAX_VALUE
;
527 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
528 var yVal
= combinedSeries
[i
][1];
529 if (yVal
!= null && isFinite(yVal
) && (!logscale
|| yVal
> 0)) {
530 yMin
= Math
.min(yMin
, yVal
);
531 yMax
= Math
.max(yMax
, yVal
);
535 // Convert Y data to log scale if needed.
536 // Also, expand the Y range to compress the mini plot a little.
537 var extraPercent
= .25;
539 yMax
= Dygraph
.log10(yMax
);
540 yMax
+= yMax
*extraPercent
;
541 yMin
= Dygraph
.log10(yMin
);
542 for (var i
= 0; i
< combinedSeries
.length
; i
++) {
543 combinedSeries
[i
][1] = Dygraph
.log10(combinedSeries
[i
][1]);
547 yRange
= yMax
- yMin
;
548 if (yRange
<= Number
.MIN_VALUE
) {
549 yExtra
= yMax
*extraPercent
;
551 yExtra
= yRange
*extraPercent
;
557 return {data
: combinedSeries
, yMin
: yMin
, yMax
: yMax
};
562 * Places the zoom handles in the proper position based on the current X data window.
564 DygraphRangeSelector
.prototype.placeZoomHandles_
= function() {
565 var xExtremes
= this.dygraph_
.xAxisExtremes();
566 var xWindowLimits
= this.dygraph_
.xAxisRange();
567 var xRange
= xExtremes
[1] - xExtremes
[0];
568 var leftPercent
= Math
.max(0, (xWindowLimits
[0] - xExtremes
[0])/xRange
);
569 var rightPercent
= Math
.max(0, (xExtremes
[1] - xWindowLimits
[1])/xRange
);
570 var leftCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*leftPercent
;
571 var rightCoord
= this.canvasRect_
.x
+ this.canvasRect_
.w
*(1 - rightPercent
);
572 var handleTop
= Math
.max(this.canvasRect_
.y
, this.canvasRect_
.y
+ (this.canvasRect_
.h
- this.leftZoomHandle_
.height
)/2);
573 var halfHandleWidth
= this.leftZoomHandle_
.width
/2;
574 this.leftZoomHandle_
.style
.left
= (leftCoord
- halfHandleWidth
) + 'px';
575 this.leftZoomHandle_
.style
.top
= handleTop
+ 'px';
576 this.rightZoomHandle_
.style
.left
= (rightCoord
- halfHandleWidth
) + 'px';
577 this.rightZoomHandle_
.style
.top
= this.leftZoomHandle_
.style
.top
;
579 this.leftZoomHandle_
.style
.visibility
= 'visible';
580 this.rightZoomHandle_
.style
.visibility
= 'visible';
585 * Draws the interactive layer in the foreground canvas.
587 DygraphRangeSelector
.prototype.drawInteractiveLayer_
= function() {
588 var ctx
= this.fgcanvas_ctx_
;
589 ctx
.clearRect(0, 0, this.canvasRect_
.w
, this.canvasRect_
.h
);
591 var width
= this.canvasRect_
.w
- margin
;
592 var height
= this.canvasRect_
.h
- margin
;
593 var zoomHandleStatus
= this.getZoomHandleStatus_();
595 ctx
.strokeStyle
= 'black';
596 if (!zoomHandleStatus
.isZoomed
) {
598 ctx
.moveTo(margin
, margin
);
599 ctx
.lineTo(margin
, height
);
600 ctx
.lineTo(width
, height
);
601 ctx
.lineTo(width
, margin
);
603 if (this.iePanOverlay_
) {
604 this.iePanOverlay_
.style
.display
= 'none';
607 leftHandleCanvasPos
= Math
.max(margin
, zoomHandleStatus
.leftHandlePos
- this.canvasRect_
.x
);
608 rightHandleCanvasPos
= Math
.min(width
, zoomHandleStatus
.rightHandlePos
- this.canvasRect_
.x
);
610 ctx
.fillStyle
= 'rgba(240, 240, 240, 0.6)';
611 ctx
.fillRect(0, 0, leftHandleCanvasPos
, this.canvasRect_
.h
);
612 ctx
.fillRect(rightHandleCanvasPos
, 0, this.canvasRect_
.w
- rightHandleCanvasPos
, this.canvasRect_
.h
);
615 ctx
.moveTo(margin
, margin
);
616 ctx
.lineTo(leftHandleCanvasPos
, margin
);
617 ctx
.lineTo(leftHandleCanvasPos
, height
);
618 ctx
.lineTo(rightHandleCanvasPos
, height
);
619 ctx
.lineTo(rightHandleCanvasPos
, margin
);
620 ctx
.lineTo(width
, margin
);
623 if (this.isUsingExcanvas_
) {
624 this.iePanOverlay_
.style
.width
= (rightHandleCanvasPos
- leftHandleCanvasPos
) + 'px';
625 this.iePanOverlay_
.style
.left
= leftHandleCanvasPos
+ 'px';
626 this.iePanOverlay_
.style
.height
= height
+ 'px';
627 this.iePanOverlay_
.style
.display
= 'inline';
634 * Returns the current zoom handle position information.
635 * @return {Object} The zoom handle status.
637 DygraphRangeSelector
.prototype.getZoomHandleStatus_
= function() {
638 var halfHandleWidth
= this.leftZoomHandle_
.width
/2;
639 var leftHandlePos
= parseInt(this.leftZoomHandle_
.style
.left
) + halfHandleWidth
;
640 var rightHandlePos
= parseInt(this.rightZoomHandle_
.style
.left
) + halfHandleWidth
;
642 leftHandlePos
: leftHandlePos
,
643 rightHandlePos
: rightHandlePos
,
644 isZoomed
: (leftHandlePos
- 1 > this.canvasRect_
.x
|| rightHandlePos
+ 1 < this.canvasRect_
.x
+this.canvasRect_
.w
)