Move to JSTD133
[dygraphs.git] / dygraph-range-selector.js
CommitLineData
ccd9d7c2
PF
1// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
2// All Rights Reserved.
3
4/**
5 * @fileoverview This file contains the DygraphRangeSelector class used to provide
6 * a timeline range selector widget for dygraphs.
7 */
8
9/**
10 * The DygraphRangeSelector class provides a timeline range selector widget.
11 * @param {Dygraph} dygraph The dygraph object
12 * @constructor
13 */
14DygraphRangeSelector = 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_();
21};
22
23/**
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.
27 */
28DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
29 this.layout_ = layout;
30 this.resize_();
31 graphDiv.appendChild(this.bgcanvas_);
32 graphDiv.appendChild(this.fgcanvas_);
33 graphDiv.appendChild(this.leftZoomHandle_);
34 graphDiv.appendChild(this.rightZoomHandle_);
35};
36
37/**
38 * Renders the static background portion of the range selector.
39 */
40DygraphRangeSelector.prototype.renderStaticLayer = function() {
41 this.resize_();
42 this.drawStaticLayer_();
43};
44
45/**
46 * Renders the interactive foreground portion of the range selector.
47 */
48DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
49 if (this.isChangingRange_) {
50 return;
51 }
52
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) {
55 var self = this;
56 setTimeout(function() { self.renderInteractiveLayer(); }, 300);
57 var retryCount = this.leftZoomHandle_.retryCount;
58 this.leftZoomHandle_.retryCount = retryCount == undefined ? 1 : retryCount+1;
59 return;
60 }
61
62 this.placeZoomHandles_();
63 this.drawInteractiveLayer_();
64};
65
66/**
67 * @private
68 * Resizes the range selector.
69 */
70DygraphRangeSelector.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
78 };
79
70be5ed1 80 var plotArea = this.layout_.getPlotArea();
ccd9d7c2
PF
81 var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
82 this.canvasRect_ = {
83 x: plotArea.x,
84 y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
85 w: plotArea.w,
86 h: this.attr_('rangeSelectorHeight')
87 };
88
89 setCanvasRect(this.bgcanvas_, this.canvasRect_);
90 setCanvasRect(this.fgcanvas_, this.canvasRect_);
91};
92
93DygraphRangeSelector.prototype.attr_ = function(name) {
94 return this.dygraph_.attr_(name);
95};
96
97/**
98 * @private
99 * Creates the background and foreground canvases.
100 */
101DygraphRangeSelector.prototype.createCanvases_ = function() {
102 this.bgcanvas_ = Dygraph.createCanvas();
88c4a47e 103 this.bgcanvas_.className = 'dygraph-rangesel-bgcanvas';
ccd9d7c2
PF
104 this.bgcanvas_.style.position = 'absolute';
105 this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_);
106
107 this.fgcanvas_ = Dygraph.createCanvas();
88c4a47e 108 this.fgcanvas_.className = 'dygraph-rangesel-fgcanvas';
ccd9d7c2
PF
109 this.fgcanvas_.style.position = 'absolute';
110 this.fgcanvas_.style.cursor = 'default';
111 this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_);
112};
113
114/**
115 * @private
116 * Creates the zoom handle elements.
117 */
118DygraphRangeSelector.prototype.createZoomHandles_ = function() {
119 var img = new Image();
88c4a47e 120 img.className = 'dygraph-rangesel-zoomhandle';
ccd9d7c2
PF
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,\
125iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
126zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
127bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
1286YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
129qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
130
131 this.leftZoomHandle_ = img;
132 this.rightZoomHandle_ = img.cloneNode(false);
133};
134
135/**
136 * @private
137 * Sets up the interaction for the range selector.
138 */
139DygraphRangeSelector.prototype.initInteraction_ = function() {
140 var self = this;
141 var topElem = this.isIE_ ? document : window;
142 var xLast = 0;
143 var handle = null;
144 var isZooming = false;
145 var isPanning = false;
146
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];
153 };
154
155 function onZoomStart(e) {
156 Dygraph.cancelEvent(e);
157 isZooming = true;
158 xLast = e.screenX;
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';
163 };
164
165 function onZoom(e) {
166 if (!isZooming) {
167 return;
168 }
169 var delX = e.screenX - xLast;
170 if (Math.abs(delX) < 4) {
171 return;
172 }
173 xLast = e.screenX;
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);
180 } else {
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);
184 }
185 handle.style.left = (newPos - halfHandleWidth) + 'px';
186 self.drawInteractiveLayer_();
187
188 // Zoom on the fly (if not using excanvas).
189 if (!self.isUsingExcanvas) {
190 doZoom();
191 }
192 };
193
194 function onZoomEnd(e) {
195 if (!isZooming) {
196 return;
197 }
198 isZooming = false;
199 Dygraph.removeEvent(topElem, 'mousemove', onZoom);
200 Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
201 self.fgcanvas_.style.cursor = 'default';
202
203 // If using excanvas, Zoom now.
204 if (self.isUsingExcanvas) {
205 doZoom();
206 }
207 };
208
209 function doZoom() {
210 try {
211 var zoomHandleStatus = self.getZoomHandleStatus_();
212 self.isChangingRange_ = true;
213 if (!zoomHandleStatus.isZoomed) {
214 self.dygraph_.doUnzoom_();
215 } else {
216 var xDataWindow = toXDataWindow(zoomHandleStatus);
217 self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
218 }
219 } finally {
220 self.isChangingRange_ = false;
221 }
222 };
223
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);
229 };
230
231 function onPanStart(e) {
232 if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
233 Dygraph.cancelEvent(e);
234 isPanning = true;
235 xLast = e.screenX;
236 Dygraph.addEvent(topElem, 'mousemove', onPan);
237 Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
238 }
239 };
240
241 function onPan(e) {
242 if (!isPanning) {
243 return;
244 }
245
246 var delX = e.screenX - xLast;
247 if (Math.abs(delX) < 4) {
248 return;
249 }
250 xLast = e.screenX;
251
252 // Move range view
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;
263 } else {
264 leftHandlePos += delX;
265 rightHandlePos += delX;
266 }
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_();
271
272 // Do pan on the fly (if not using excanvas).
273 if (!self.isUsingExcanvas) {
274 doPan();
275 }
276 };
277
278 function onPanEnd(e) {
279 if (!isPanning) {
280 return;
281 }
282 isPanning = false;
283 Dygraph.removeEvent(topElem, 'mousemove', onPan);
284 Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
285 // If using excanvas, do pan now.
286 if (self.isUsingExcanvas) {
287 doPan();
288 }
289 };
290
291 function doPan() {
292 try {
293 self.isChangingRange_ = true;
294 self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
295 self.dygraph_.drawGraph_(false);
296 } finally {
297 self.isChangingRange_ = false;
298 }
299 };
300
301 function onCanvasMouseMove(e) {
302 if (isZooming || isPanning) {
303 return;
304 }
305 var cursor = isMouseInPanZone(e) ? 'move' : 'default';
306 if (cursor != self.fgcanvas_.style.cursor) {
307 self.fgcanvas_.style.cursor = cursor;
308 }
309 };
310
311 var interactionModel = {
312 mousedown: function(event, g, context) {
313 context.initializeMouseDown(event, g, context);
314 Dygraph.startPan(event, g, context);
315 },
316 mousemove: function(event, g, context) {
317 if (context.isPanning) {
318 Dygraph.movePan(event, g, context);
319 }
320 },
321 mouseup: function(event, g, context) {
322 if (context.isPanning) {
323 Dygraph.endPan(event, g, context);
324 }
325 }
326 };
327
328 this.dygraph_.attrs_.interactionModel = interactionModel;
329 this.dygraph_.attrs_.panEdgeFraction = .0001;
330
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);
335};
336
337/**
338 * @private
339 * Draws the static layer in the background canvas.
340 */
341DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
342 var ctx = this.bgcanvas_ctx_;
343 ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
344 var margin = .5;
345 try {
346 this.drawMiniPlot_();
347 } catch(ex) {
348 }
349 ctx.strokeStyle = 'lightgray';
350 if (false) {
351 ctx.strokeRect(margin, margin, this.canvasRect_.w-margin, this.canvasRect_.h-margin);
352 } else {
353 ctx.beginPath();
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);
358 ctx.stroke();
359 }
360};
361
362
363/**
364 * @private
365 * Draws the mini plot in the background canvas.
366 */
367DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
368 var fillStyle = this.attr_('rangeSelectorPlotFillColor');
369 var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
370 if (!fillStyle && !strokeStyle) {
371 return;
372 }
373
374 var combinedSeriesData = this.computeCombinedSeriesAndLimits_();
375 var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin;
376
377 // Draw the mini plot.
378 var ctx = this.bgcanvas_ctx_;
379 var margin = .5;
380
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;
387
388 ctx.beginPath();
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)) {
395 ctx.lineTo(x, y);
396 }
397 }
398 ctx.lineTo(canvasWidth, canvasHeight);
399 ctx.closePath();
400
401 if (fillStyle) {
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;
406 ctx.fill();
407 }
408
409 if (strokeStyle) {
410 this.bgcanvas_ctx_.strokeStyle = strokeStyle;
411 this.bgcanvas_ctx_.lineWidth = 1.5;
412 ctx.stroke();
413 }
414};
415
416/**
417 * @private
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.
420 */
421DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
422 var data = this.dygraph_.rawData_;
423 var logscale = this.attr_('logscale');
424
425 // Create a combined series (average of all series values).
426 var combinedSeries = [];
427 var sum;
428 var count;
429 var mutipleValues = typeof data[0][1] != 'number';
430
431 if (mutipleValues) {
432 sum = [];
433 count = [];
434 for (var k = 0; k < data[0][1].length; k++) {
435 sum.push(0);
436 count.push(0);
437 }
438 mutipleValues = true;
439 }
440
441 for (var i = 0; i < data.length; i++) {
442 var dataPoint = data[i];
443 var xVal = dataPoint[0];
444 var yVal;
445
446 if (mutipleValues) {
447 for (var k = 0; k < sum.length; k++) {
448 sum[k] = count[k] = 0;
449 }
450 } else {
451 sum = count = 0;
452 }
453
454 for (var j = 1; j < dataPoint.length; j++) {
455 if (this.dygraph_.visibility()[j-1]) {
456 if (mutipleValues) {
457 for (var k = 0; k < sum.length; k++) {
458 var y = dataPoint[j][k];
459 if (y == null || isNaN(y)) continue;
460 sum[k] += y;
461 count[k]++;
462 }
463 } else {
464 var y = dataPoint[j];
465 if (y == null || isNaN(y)) continue;
466 sum += y;
467 count++;
468 }
469 }
470 }
471
472 if (mutipleValues) {
473 for (var k = 0; k < sum.length; k++) {
474 sum[k] /= count[k];
475 }
476 yVal = sum.slice(0);
477 } else {
478 yVal = sum/count;
479 }
480
481 combinedSeries.push([xVal, yVal]);
482 }
483
484 // Account for roll period, fractions.
485 combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
486
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];
491 }
492 }
493
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);
502 }
503 }
504
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;
508 if (logscale) {
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]);
514 }
515 } else {
516 var yExtra;
517 yRange = yMax - yMin;
518 if (yRange <= Number.MIN_VALUE) {
519 yExtra = yMax*extraPercent;
520 } else {
521 yExtra = yRange*extraPercent;
522 }
523 yMax += yExtra;
524 yMin -= yExtra;
525 }
526
527 return {data: combinedSeries, yMin: yMin, yMax: yMax};
528};
529
530/**
531 * @private
532 * Places the zoom handles in the proper position based on the current X data window.
533 */
534DygraphRangeSelector.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;
548
549 this.leftZoomHandle_.style.visibility = 'visible';
550 this.rightZoomHandle_.style.visibility = 'visible';
551};
552
553/**
554 * @private
555 * Draws the interactive layer in the foreground canvas.
556 */
557DygraphRangeSelector.prototype.drawInteractiveLayer_ = function() {
558 var ctx = this.fgcanvas_ctx_;
559 ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
560 var margin = 1;
561 var width = this.canvasRect_.w - margin;
562 var height = this.canvasRect_.h - margin;
563 var zoomHandleStatus = this.getZoomHandleStatus_();
564
565 ctx.strokeStyle = 'black';
566 if (!zoomHandleStatus.isZoomed) {
567 ctx.beginPath();
568 ctx.moveTo(margin, margin);
569 ctx.lineTo(margin, height);
570 ctx.lineTo(width, height);
571 ctx.lineTo(width, margin);
572 ctx.stroke();
573 } else {
574 leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x);
575 rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x);
576
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);
580
581 ctx.beginPath();
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);
588 ctx.stroke();
589 }
590};
591
592/**
593 * @private
594 * Returns the current zoom handle position information.
595 * @return {Object} The zoom handle status.
596 */
597DygraphRangeSelector.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;
601 return {
602 leftHandlePos: leftHandlePos,
603 rightHandlePos: rightHandlePos,
604 isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
605 };
606};