A range selector widget for dygraphs.
authorPaul Felix <paul.eric.felix@gmail.com>
Thu, 25 Aug 2011 21:28:11 +0000 (14:28 -0700)
committerDan Vanderkam <dan@dygraphs.com>
Thu, 25 Aug 2011 21:28:11 +0000 (14:28 -0700)
This activates a more discoverable UI for panning and zooming, very similar to
the one used by the Annotated Timeline widget on Google Finance. Enable it by
passing 'showRangeSelector: true' in the constructor options.

13 files changed:
auto_tests/tests/range_selector.js [new file with mode: 0644]
auto_tests/tests/tickers.js
dygraph-canvas.js
dygraph-dev.js
dygraph-layout.js
dygraph-options-reference.js
dygraph-range-selector.js [new file with mode: 0644]
dygraph-tickers.js
dygraph-utils.js
dygraph.js
generate-combined.sh
jsTestDriver.conf
tests/range-selector.html [new file with mode: 0644]

diff --git a/auto_tests/tests/range_selector.js b/auto_tests/tests/range_selector.js
new file mode 100644 (file)
index 0000000..4ddfcb1
--- /dev/null
@@ -0,0 +1,140 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+/**
+ * @fileoverview Regression tests for range selector.
+ * @author paul.eric.felix@gmail.com (Paul Felix)
+ */
+var RangeSelectorTestCase = TestCase("range-selector");
+
+RangeSelectorTestCase.prototype.setUp = function() {
+  document.body.innerHTML = "<div id='graph'></div>";
+};
+
+RangeSelectorTestCase.prototype.tearDown = function() {
+};
+
+RangeSelectorTestCase.prototype.testRangeSelector = function() {
+  var opts = {
+    width: 480,
+    height: 320,
+    showRangeSelector: true
+  };
+  var data = [
+               [1, 10],
+               [2, 15],
+               [3, 10],
+               [4, 15],
+               [5, 10],
+               [6, 15],
+               [7, 10],
+               [8, 15],
+               [9, 10]
+             ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+  this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithErrorBars = function() {
+  var opts = {
+    width: 480,
+    height: 320,
+    errorBars: true,
+    showRangeSelector: true
+  };
+  var data = [
+               [1, [10, 10]],
+               [2, [15, 10]],
+               [3, [10, 10]],
+               [4, [15, 10]],
+               [5, [10, 10]],
+               [6, [15, 20]],
+               [7, [10, 20]],
+               [8, [15, 20]],
+               [9, [10, 20]]
+             ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+  this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithCustomBars = function() {
+  var opts = {
+    width: 480,
+    height: 320,
+    customBars: true,
+    showRangeSelector: true
+  };
+  var data = [
+               [1, [10,  10, 100]],
+               [2, [15,  20, 110]],
+               [3, [10,  30, 100]],
+               [4, [15,  40, 110]],
+               [5, [10, 120, 100]],
+               [6, [15,  50, 110]],
+               [7, [10,  70, 100]],
+               [8, [15,  90, 110]],
+               [9, [10,  50, 100]]
+             ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+  this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithLogScale = function() {
+  var opts = {
+    width: 480,
+    height: 320,
+    logscale: true,
+    showRangeSelector: true
+  };
+  var data = [
+               [1, 10],
+               [2, 15],
+               [3, 10],
+               [4, 15],
+               [5, 10],
+               [6, 15],
+               [7, 10],
+               [8, 15],
+               [9, 10]
+             ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+  this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorOptions = function() {
+  var opts = {
+    width: 480,
+    height: 320,
+    showRangeSelector: true,
+    rangeSelectorHeight: 30,
+    rangeSelectorPlotFillColor: 'lightyellow',
+    rangeSelectorPlotStyleColor: 'yellow'
+  };
+  var data = [
+               [1, 10],
+               [2, 15],
+               [3, 10],
+               [4, 15],
+               [5, 10],
+               [6, 15],
+               [7, 10],
+               [8, 15],
+               [9, 10]
+             ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+  this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.assertGraphExistence = function(g, graph) {
+  assertNotNull(g);
+  var zoomhandles = graph.getElementsByClassName('dygraph_rangesel_zoomhandle');
+  assertEquals(2, zoomhandles.length);
+  var bgcanvas = graph.getElementsByClassName('dygraph_rangesel_bgcanvas');
+  assertEquals(1, bgcanvas.length);
+  var fgcanvas = graph.getElementsByClassName('dygraph_rangesel_fgcanvas');
+  assertEquals(1, fgcanvas.length);
+}
index c3ae456..2d7fffc 100644 (file)
@@ -69,6 +69,10 @@ TickerTestCase.prototype.testAllDateTickers = function() {
   assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1191211200000,"label":"Oct 07"}], Dygraph.dateTicker(1167627600000, 1199077200000, 480, this.createOptionsViewForAxis('x')));
   assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1191211200000,"label":"Oct 07"}], Dygraph.dateTicker(1167627600000, 1199077200000, 600, this.createOptionsViewForAxis('x')));
   // assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1170306000000,"label":"Feb 07"},{"v":1172725200000,"label":"Mar 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1177992000000,"label":"May 07"},{"v":1180670400000,"label":"Jun 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1185940800000,"label":"Aug 07"},{"v":1188619200000,"label":"Sep 07"},{"v":1191211200000,"label":"Oct 07"},{"v":1193889600000,"label":"Nov 07"},{"v":1196485200000,"label":"Dec 07"},{"v":null,"label":"undefined NaN"}], Dygraph.dateTicker(1167627600000, 1199077200000, 800, this.createOptionsViewForAxis('x')));
+
+  // Test monthly for time span starting Dec 31, 2010.
+  assertEquals([{"v":1293858000000,"label":"Jan 11"},{"v":1296536400000,"label":"Feb 11"},{"v":1298955600000,"label":"Mar 11"},{"v":1301630400000,"label":"Apr 11"},{"v":1304222400000,"label":"May 11"},{"v":1306900800000,"label":"Jun 11"},{"v":1309492800000,"label":"Jul 11"},{"v":1312171200000,"label":"Aug 11"}], Dygraph.dateTicker(1293771600000, 1312862400000, 727, this.createOptionsViewForAxis('x')));
+
   assertEquals([{"v":1201842000000,"label":"01Feb"},{"v":1201928400000,"label":"02Feb"},{"v":1202014800000,"label":"03Feb"},{"v":1202101200000,"label":"04Feb"},{"v":1202187600000,"label":"05Feb"},{"v":1202274000000,"label":"06Feb"}], Dygraph.dateTicker(1201842000000, 1202274000000, 700, this.createOptionsViewForAxis('x')));
   assertEquals([{"v":1210132800000,"label":"07May"},{"v":1210154400000,"label":"06:00"},{"v":1210176000000,"label":"12:00"},{"v":1210197600000,"label":"18:00"},{"v":1210219200000,"label":"08May"},{"v":1210240800000,"label":"06:00"},{"v":1210262400000,"label":"12:00"},{"v":1210284000000,"label":"18:00"},{"v":1210305600000,"label":"09May"}], Dygraph.dateTicker(1210132800000, 1210305600000, 480, this.createOptionsViewForAxis('x')));
   assertEquals([{"v":1210132800000,"label":"07May"},{"v":1210219200000,"label":"08May"},{"v":1210305600000,"label":"09May"},{"v":1210392000000,"label":"10May"},{"v":1210478400000,"label":"11May"}], Dygraph.dateTicker(1210132800000, 1210478400000, 480, this.createOptionsViewForAxis('x')));
index 90d1b4e..f4140e6 100644 (file)
@@ -44,7 +44,7 @@ DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
   this.annotations = new Array();
   this.chartLabels = {};
 
-  this.area = this.computeArea_();
+  this.area = layout.plotArea;
   this.container.style.position = "relative";
   this.container.style.width = this.width + "px";
 
@@ -65,56 +65,6 @@ DygraphCanvasRenderer.prototype.attr_ = function(x) {
   return this.dygraph_.attr_(x);
 };
 
-// Compute the box which the chart should be drawn in. This is the canvas's
-// box, less space needed for axis and chart labels.
-// TODO(danvk): this belongs in DygraphLayout.
-DygraphCanvasRenderer.prototype.computeArea_ = function() {
-  var area = {
-    // TODO(danvk): per-axis setting.
-    x: 0,
-    y: 0
-  };
-  if (this.attr_('drawYAxis')) {
-   area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
-  }
-
-  area.w = this.width - area.x - this.attr_('rightGap');
-  area.h = this.height;
-  if (this.attr_('drawXAxis')) {
-    if (this.attr_('xAxisHeight')) {
-      area.h -= this.attr_('xAxisHeight');
-    } else {
-      area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
-    }
-  }
-
-  // Shrink the drawing area to accomodate additional y-axes.
-  if (this.dygraph_.numAxes() == 2) {
-    // TODO(danvk): per-axis setting.
-    area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
-  } else if (this.dygraph_.numAxes() > 2) {
-    this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
-                        "to use " + this.dygraph_.numAxes() + ")");
-  }
-
-  // Add space for chart labels: title, xlabel and ylabel.
-  if (this.attr_('title')) {
-    area.h -= this.attr_('titleHeight');
-    area.y += this.attr_('titleHeight');
-  }
-  if (this.attr_('xlabel')) {
-    area.h -= this.attr_('xLabelHeight');
-  }
-  if (this.attr_('ylabel')) {
-    // It would make sense to shift the chart here to make room for the y-axis
-    // label, but the default yAxisLabelWidth is large enough that this results
-    // in overly-padded charts. The y-axis label should fit fine. If it
-    // doesn't, the yAxisLabelWidth option can be increased.
-  }
-
-  return area;
-};
-
 DygraphCanvasRenderer.prototype.clear = function() {
   if (this.isIE) {
     // VML takes a while to start up, so we just poll every this.IEDelay
@@ -240,7 +190,7 @@ DygraphCanvasRenderer.prototype.render = function() {
   // Do the ordinary rendering, as before
   this._renderLineChart();
   this._renderAxis();
-  this._renderChartLabels(); 
+  this._renderChartLabels();
   this._renderAnnotations();
 };
 
@@ -261,6 +211,7 @@ DygraphCanvasRenderer.prototype._renderAxis = function() {
     color: this.attr_('axisLabelColor'),
     width: this.attr_('axisLabelWidth') + "px",
     // height: this.attr_('axisLabelFontSize') + 2 + "px",
+    lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
     overflow: "hidden"
   };
   var makeDiv = function(txt, axis, prec_axis) {
@@ -602,7 +553,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
   var isNullOrNaN = function(x) {
     return (x === null || isNaN(x));
   };
-  
+
   // TODO(danvk): use this.attr_ for many of these.
   var context = this.elementContext;
   var fillAlpha = this.attr_('fillAlpha');
index 006eccc..1f6005a 100644 (file)
@@ -24,8 +24,9 @@
     "dygraph-utils.js",
     "dygraph-gviz.js",
     "dygraph-interaction-model.js",
-    "dygraph-options-reference.js",  // Shouldn't be included in generate-combined.sh
-    "dygraph-tickers.js"
+    "dygraph-range-selector.js",
+    "dygraph-tickers.js",
+    "dygraph-options-reference.js"  // Shouldn't be included in generate-combined.sh
   ];
 
   for (var i = 0; i < source_files.length; i++) {
index 17de467..915d235 100644 (file)
@@ -30,6 +30,7 @@ DygraphLayout = function(dygraph) {
   this.datasets = new Array();
   this.annotations = new Array();
   this.yAxes_ = null;
+  this.plotArea = this.computePlotArea_();
 
   // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
   // yticks are outputs. Clean this up.
@@ -45,6 +46,60 @@ DygraphLayout.prototype.addDataset = function(setname, set_xy) {
   this.datasets[setname] = set_xy;
 };
 
+// Compute the box which the chart should be drawn in. This is the canvas's
+// box, less space needed for axis and chart labels.
+DygraphLayout.prototype.computePlotArea_ = function() {
+  var area = {
+    // TODO(danvk): per-axis setting.
+    x: 0,
+    y: 0
+  };
+  if (this.attr_('drawYAxis')) {
+   area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
+  }
+
+  area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap');
+  area.h = this.dygraph_.height_;
+  if (this.attr_('drawXAxis')) {
+    if (this.attr_('xAxisHeight')) {
+      area.h -= this.attr_('xAxisHeight');
+    } else {
+      area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+    }
+  }
+
+  // Shrink the drawing area to accomodate additional y-axes.
+  if (this.dygraph_.numAxes() == 2) {
+    // TODO(danvk): per-axis setting.
+    area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
+  } else if (this.dygraph_.numAxes() > 2) {
+    this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
+                        "to use " + this.dygraph_.numAxes() + ")");
+  }
+
+  // Add space for chart labels: title, xlabel and ylabel.
+  if (this.attr_('title')) {
+    area.h -= this.attr_('titleHeight');
+    area.y += this.attr_('titleHeight');
+  }
+  if (this.attr_('xlabel')) {
+    area.h -= this.attr_('xLabelHeight');
+  }
+  if (this.attr_('ylabel')) {
+    // It would make sense to shift the chart here to make room for the y-axis
+    // label, but the default yAxisLabelWidth is large enough that this results
+    // in overly-padded charts. The y-axis label should fit fine. If it
+    // doesn't, the yAxisLabelWidth option can be increased.
+  }
+
+  // Add space for range selector, if needed.
+  if (this.attr_('showRangeSelector')) {
+    area.h -= this.attr_('rangeSelectorHeight') + 4;
+  }
+
+  return area;
+};
+
 DygraphLayout.prototype.setAnnotations = function(ann) {
   // The Dygraph object's annotations aren't parsed. We parse them here and
   // save a copy. If there is no parser, then the user must be using raw format.
@@ -101,7 +156,7 @@ DygraphLayout.prototype._evaluateLimits = function() {
       if (series.length > 1) {
         var x1 = series[0][0];
         if (!this.minxval || x1 < this.minxval) this.minxval = x1;
-  
+
         var x2 = series[series.length - 1][0];
         if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
       }
@@ -254,7 +309,7 @@ DygraphLayout.prototype._evaluateAnnotations = function() {
   if (!this.annotations || !this.annotations.length) {
     return;
   }
-  
+
   // TODO(antrob): loop through annotations not points.
   for (var i = 0; i < this.points.length; i++) {
     var p = this.points[i];
@@ -280,25 +335,25 @@ DygraphLayout.prototype.removeAllDatasets = function() {
  */
 DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
   var point = this.points[idx];
-  
+
   // Clone the point since we modify it
-  var unstackedPoint = {};  
+  var unstackedPoint = {};
   for (var i in point) {
     unstackedPoint[i] = point[i];
   }
-  
+
   if (!this.attr_("stackedGraph")) {
     return unstackedPoint;
   }
-  
-  // The unstacked yval is equal to the current yval minus the yval of the 
+
+  // The unstacked yval is equal to the current yval minus the yval of the
   // next point at the same xval.
   for (var i = idx+1; i < this.points.length; i++) {
     if (this.points[i].xval == point.xval) {
-      unstackedPoint.yval -= this.points[i].yval; 
+      unstackedPoint.yval -= this.points[i].yval;
       break;
     }
   }
-  
+
   return unstackedPoint;
-}  
+}
index 9991484..88ec2a1 100644 (file)
@@ -581,6 +581,30 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "labels": [ "Debugging" ],
     "type": "string",
     "description": "Set this option to log timing information. The value of the option will be logged along with the timimg, so that you can distinguish multiple dygraphs on the same page."
+  },
+  "showRangeSelector": {
+    "default": "false",
+    "labels": ["Interactive Elements"],
+    "type": "boolean",
+    "description": "Show the range selector widget. This option can only be specified at Dygraph creation time."
+  },
+  "rangeSelectorHeight": {
+    "default": "40",
+    "labels": ["Interactive Elements"],
+    "type": "integer",
+    "description": "Height, in pixels, of the range selector widget. This option can only be specified at Dygraph creation time."
+  },
+  "rangeSelectorPlotStrokeColor": {
+    "default": "#808FAB",
+    "labels": ["Interactive Elements"],
+    "type": "string",
+    "description": "The range selector mini plot stroke color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off stroke."
+  },
+  "rangeSelectorPlotFillColor": {
+    "default": "#A7B1C4",
+    "labels": ["Interactive Elements"],
+    "type": "string",
+    "description": "The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill."
   }
 }
 ;  // </JSON>
@@ -593,7 +617,7 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
 (function() {
   var warn = function(msg) { if (console) console.warn(msg); };
   var flds = ['type', 'default', 'description'];
-  var valid_cats = [ 
+  var valid_cats = [
    'Annotations',
    'Axis display',
    'Chart labels',
diff --git a/dygraph-range-selector.js b/dygraph-range-selector.js
new file mode 100644 (file)
index 0000000..525949b
--- /dev/null
@@ -0,0 +1,606 @@
+// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @fileoverview This file contains the DygraphRangeSelector class used to provide
+ * a timeline range selector widget for dygraphs.
+ */
+
+/**
+ * The DygraphRangeSelector class provides a timeline range selector widget.
+ * @param {Dygraph} dygraph The dygraph object
+ * @constructor
+ */
+DygraphRangeSelector = function(dygraph) {
+  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
+  this.isUsingExcanvas = typeof(G_vmlCanvasManager) != 'undefined';
+  this.dygraph_ = dygraph;
+  this.createCanvases_();
+  this.createZoomHandles_();
+  this.initInteraction_();
+};
+
+/**
+ * Adds the range selector to the dygraph.
+ * @param {Object} graphDiv The container div for the range selector.
+ * @param {DygraphLayout} layout The DygraphLayout object for this graph.
+ */
+DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
+  this.layout_ = layout;
+  this.resize_();
+  graphDiv.appendChild(this.bgcanvas_);
+  graphDiv.appendChild(this.fgcanvas_);
+  graphDiv.appendChild(this.leftZoomHandle_);
+  graphDiv.appendChild(this.rightZoomHandle_);
+};
+
+/**
+ * Renders the static background portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderStaticLayer = function() {
+  this.resize_();
+  this.drawStaticLayer_();
+};
+
+/**
+ * Renders the interactive foreground portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
+  if (this.isChangingRange_) {
+    return;
+  }
+
+  // The zoom handle image may not be loaded yet. May need to try again later.
+  if (this.leftZoomHandle_.height == 0 && this.leftZoomHandle_.retryCount != 5) {
+    var self = this;
+    setTimeout(function() { self.renderInteractiveLayer(); }, 300);
+    var retryCount = this.leftZoomHandle_.retryCount;
+    this.leftZoomHandle_.retryCount = retryCount == undefined ? 1 : retryCount+1;
+    return;
+  }
+
+  this.placeZoomHandles_();
+  this.drawInteractiveLayer_();
+};
+
+/**
+ * @private
+ * Resizes the range selector.
+ */
+DygraphRangeSelector.prototype.resize_ = function() {
+  function setCanvasRect(canvas, rect) {
+    canvas.style.top = rect.y + 'px';
+    canvas.style.left = rect.x + 'px';
+    canvas.width = rect.w;
+    canvas.height = rect.h;
+    canvas.style.width = canvas.width + 'px';    // for IE
+    canvas.style.height = canvas.height + 'px';  // for IE
+  };
+
+  var plotArea = this.layout_.plotArea;
+  var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+  this.canvasRect_ = {
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
+    w: plotArea.w,
+    h: this.attr_('rangeSelectorHeight')
+  };
+
+  setCanvasRect(this.bgcanvas_, this.canvasRect_);
+  setCanvasRect(this.fgcanvas_, this.canvasRect_);
+};
+
+DygraphRangeSelector.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+/**
+ * @private
+ * Creates the background and foreground canvases.
+ */
+DygraphRangeSelector.prototype.createCanvases_ = function() {
+  this.bgcanvas_ = Dygraph.createCanvas();
+  this.bgcanvas_.className = 'dygraph_rangesel_bgcanvas';
+  this.bgcanvas_.style.position = 'absolute';
+  this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_);
+
+  this.fgcanvas_ = Dygraph.createCanvas();
+  this.fgcanvas_.className = 'dygraph_rangesel_fgcanvas';
+  this.fgcanvas_.style.position = 'absolute';
+  this.fgcanvas_.style.cursor = 'default';
+  this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_);
+};
+
+/**
+ * @private
+ * Creates the zoom handle elements.
+ */
+DygraphRangeSelector.prototype.createZoomHandles_ = function() {
+  var img = new Image();
+  img.className = 'dygraph_rangesel_zoomhandle';
+  img.style.position = 'absolute';
+  img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place.
+  img.style.cursor = 'col-resize';
+  img.src = 'data:image/png;base64,\
+iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
+zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
+bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
+6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
+qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
+
+  this.leftZoomHandle_ = img;
+  this.rightZoomHandle_ = img.cloneNode(false);
+};
+
+/**
+ * @private
+ * Sets up the interaction for the range selector.
+ */
+DygraphRangeSelector.prototype.initInteraction_ = function() {
+  var self = this;
+  var topElem = this.isIE_ ? document : window;
+  var xLast = 0;
+  var handle = null;
+  var isZooming = false;
+  var isPanning = false;
+
+  function toXDataWindow(zoomHandleStatus) {
+    var xDataLimits = self.dygraph_.xAxisExtremes();
+    var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w;
+    var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact;
+    var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact;
+    return [xDataMin, xDataMax];
+  };
+
+  function onZoomStart(e) {
+    Dygraph.cancelEvent(e);
+    isZooming = true;
+    xLast = e.screenX;
+    handle = e.target ? e.target : e.srcElement;
+    Dygraph.addEvent(topElem, 'mousemove', onZoom);
+    Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'col-resize';
+  };
+
+  function onZoom(e) {
+    if (!isZooming) {
+      return;
+    }
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+    var zoomHandleStatus = self.getZoomHandleStatus_();
+    var halfHandleWidth = Math.round(handle.width/2);
+    if (handle == self.leftZoomHandle_) {
+      var newPos = zoomHandleStatus.leftHandlePos + delX;
+      newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3);
+      newPos = Math.max(newPos, self.canvasRect_.x);
+    } else {
+      var newPos = zoomHandleStatus.rightHandlePos + delX;
+      newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w);
+      newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3);
+    }
+    handle.style.left = (newPos - halfHandleWidth) + 'px';
+    self.drawInteractiveLayer_();
+
+    // Zoom on the fly (if not using excanvas).
+    if (!self.isUsingExcanvas) {
+      doZoom();
+    }
+  };
+
+  function onZoomEnd(e) {
+    if (!isZooming) {
+      return;
+    }
+    isZooming = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
+    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'default';
+
+    // If using excanvas, Zoom now.
+    if (self.isUsingExcanvas) {
+      doZoom();
+    }
+  };
+
+  function doZoom() {
+    try {
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      self.isChangingRange_ = true;
+      if (!zoomHandleStatus.isZoomed) {
+        self.dygraph_.doUnzoom_();
+      } else {
+        var xDataWindow = toXDataWindow(zoomHandleStatus);
+        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
+      }
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  function isMouseInPanZone(e) {
+    // Getting clientX directly from the event is not accurate enough :(
+    var clientX = self.canvasRect_.x + (e.layerX != undefined ? e.layerX : e.offsetX);
+    var zoomHandleStatus = self.getZoomHandleStatus_();
+    return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
+  };
+
+  function onPanStart(e) {
+    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
+      Dygraph.cancelEvent(e);
+      isPanning = true;
+      xLast = e.screenX;
+      Dygraph.addEvent(topElem, 'mousemove', onPan);
+      Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+    }
+  };
+
+  function onPan(e) {
+    if (!isPanning) {
+      return;
+    }
+
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+
+    // Move range view
+    var zoomHandleStatus = self.getZoomHandleStatus_();
+    var leftHandlePos = zoomHandleStatus.leftHandlePos;
+    var rightHandlePos = zoomHandleStatus.rightHandlePos;
+    var rangeSize = rightHandlePos - leftHandlePos;
+    if (leftHandlePos + delX <= self.canvasRect_.x) {
+      leftHandlePos = self.canvasRect_.x;
+      rightHandlePos = leftHandlePos + rangeSize;
+    } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) {
+      rightHandlePos = self.canvasRect_.x + self.canvasRect_.w;
+      leftHandlePos = rightHandlePos - rangeSize;
+    } else {
+      leftHandlePos += delX;
+      rightHandlePos += delX;
+    }
+    var halfHandleWidth = Math.round(self.leftZoomHandle_.width/2);
+    self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px';
+    self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px';
+    self.drawInteractiveLayer_();
+
+    // Do pan on the fly (if not using excanvas).
+    if (!self.isUsingExcanvas) {
+      doPan();
+    }
+  };
+
+  function onPanEnd(e) {
+    if (!isPanning) {
+      return;
+    }
+    isPanning = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onPan);
+    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
+    // If using excanvas, do pan now.
+    if (self.isUsingExcanvas) {
+      doPan();
+    }
+  };
+
+  function doPan() {
+    try {
+      self.isChangingRange_ = true;
+      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
+      self.dygraph_.drawGraph_(false);
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  function onCanvasMouseMove(e) {
+    if (isZooming || isPanning) {
+      return;
+    }
+    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
+    if (cursor != self.fgcanvas_.style.cursor) {
+      self.fgcanvas_.style.cursor = cursor;
+    }
+  };
+
+  var interactionModel = {
+    mousedown: function(event, g, context) {
+      context.initializeMouseDown(event, g, context);
+      Dygraph.startPan(event, g, context);
+    },
+    mousemove: function(event, g, context) {
+      if (context.isPanning) {
+        Dygraph.movePan(event, g, context);
+      }
+    },
+    mouseup: function(event, g, context) {
+      if (context.isPanning) {
+        Dygraph.endPan(event, g, context);
+      }
+    }
+  };
+
+  this.dygraph_.attrs_.interactionModel = interactionModel;
+  this.dygraph_.attrs_.panEdgeFraction = .0001;
+
+  Dygraph.addEvent(this.leftZoomHandle_, 'dragstart', onZoomStart);
+  Dygraph.addEvent(this.rightZoomHandle_, 'dragstart', onZoomStart);
+  Dygraph.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
+  Dygraph.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
+};
+
+/**
+ * @private
+ * Draws the static layer in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
+  var ctx = this.bgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  var margin = .5;
+  try {
+    this.drawMiniPlot_();
+  } catch(ex) {
+  }
+  ctx.strokeStyle = 'lightgray';
+  if (false) {
+    ctx.strokeRect(margin, margin, this.canvasRect_.w-margin, this.canvasRect_.h-margin);
+  } else {
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(margin, this.canvasRect_.h-margin);
+    ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin);
+    ctx.lineTo(this.canvasRect_.w-margin, margin);
+    ctx.stroke();
+  }
+};
+
+
+/**
+ * @private
+ * Draws the mini plot in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
+  var fillStyle = this.attr_('rangeSelectorPlotFillColor');
+  var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
+  if (!fillStyle && !strokeStyle) {
+    return;
+  }
+
+  var combinedSeriesData = this.computeCombinedSeriesAndLimits_();
+  var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin;
+
+  // Draw the mini plot.
+  var ctx = this.bgcanvas_ctx_;
+  var margin = .5;
+
+  var xExtremes = this.dygraph_.xAxisExtremes();
+  var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30);
+  var xFact = (this.canvasRect_.w - margin)/xRange;
+  var yFact = (this.canvasRect_.h - margin)/yRange;
+  var canvasWidth = this.canvasRect_.w - margin;
+  var canvasHeight = this.canvasRect_.h - margin;
+
+  ctx.beginPath();
+  ctx.moveTo(margin, canvasHeight);
+  for (var i = 0; i < combinedSeriesData.data.length; i++) {
+    var dataPoint = combinedSeriesData.data[i];
+    var x = (dataPoint[0] - xExtremes[0])*xFact;
+    var y = canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact;
+    if (isFinite(x) && isFinite(y)) {
+      ctx.lineTo(x, y);
+    }
+  }
+  ctx.lineTo(canvasWidth, canvasHeight);
+  ctx.closePath();
+
+  if (fillStyle) {
+    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
+    lingrad.addColorStop(0, 'white');
+    lingrad.addColorStop(1, fillStyle);
+    this.bgcanvas_ctx_.fillStyle = lingrad;
+    ctx.fill();
+  }
+
+  if (strokeStyle) {
+    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
+    this.bgcanvas_ctx_.lineWidth = 1.5;
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Computes and returns the combinded series data along with min/max for the mini plot.
+ * @return {Object} An object containing combinded series array, ymin, ymax.
+ */
+DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
+  var data = this.dygraph_.rawData_;
+  var logscale = this.attr_('logscale');
+
+  // Create a combined series (average of all series values).
+  var combinedSeries = [];
+  var sum;
+  var count;
+  var mutipleValues = typeof data[0][1] != 'number';
+
+  if (mutipleValues) {
+    sum = [];
+    count = [];
+    for (var k = 0; k < data[0][1].length; k++) {
+      sum.push(0);
+      count.push(0);
+    }
+    mutipleValues = true;
+  }
+
+  for (var i = 0; i < data.length; i++) {
+    var dataPoint = data[i];
+    var xVal = dataPoint[0];
+    var yVal;
+
+    if (mutipleValues) {
+      for (var k = 0; k < sum.length; k++) {
+        sum[k] = count[k] = 0;
+      }
+    } else {
+      sum = count = 0;
+    }
+
+    for (var j = 1; j < dataPoint.length; j++) {
+      if (this.dygraph_.visibility()[j-1]) {
+        if (mutipleValues) {
+          for (var k = 0; k < sum.length; k++) {
+            var y = dataPoint[j][k];
+            if (y == null || isNaN(y)) continue;
+            sum[k] += y;
+            count[k]++;
+          }
+        } else {
+          var y = dataPoint[j];
+          if (y == null || isNaN(y)) continue;
+          sum += y;
+          count++;
+        }
+      }
+    }
+
+    if (mutipleValues) {
+      for (var k = 0; k < sum.length; k++) {
+        sum[k] /= count[k];
+      }
+      yVal = sum.slice(0);
+    } else {
+      yVal = sum/count;
+    }
+
+    combinedSeries.push([xVal, yVal]);
+  }
+
+  // Account for roll period, fractions.
+  combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
+
+  if (typeof combinedSeries[0][1] != 'number') {
+    for (var i = 0; i < combinedSeries.length; i++) {
+      var yVal = combinedSeries[i][1];
+      combinedSeries[i][1] = yVal[0];
+    }
+  }
+
+  // Compute the y range.
+  var yMin = Number.MAX_VALUE;
+  var yMax = -Number.MAX_VALUE;
+  for (var i = 0; i < combinedSeries.length; i++) {
+    var yVal = combinedSeries[i][1];
+    if (!logscale || yVal > 0) {
+      yMin = Math.min(yMin, yVal);
+      yMax = Math.max(yMax, yVal);
+    }
+  }
+
+  // Convert Y data to log scale if needed.
+  // Also, expand the Y range to compress the mini plot a little.
+  var extraPercent = .25;
+  if (logscale) {
+    yMax = Dygraph.log10(yMax);
+    yMax += yMax*extraPercent;
+    yMin = Dygraph.log10(yMin);
+    for (var i = 0; i < combinedSeries.length; i++) {
+      combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]);
+    }
+  } else {
+    var yExtra;
+    yRange = yMax - yMin;
+    if (yRange <= Number.MIN_VALUE) {
+      yExtra = yMax*extraPercent;
+    } else {
+      yExtra = yRange*extraPercent;
+    }
+    yMax += yExtra;
+    yMin -= yExtra;
+  }
+
+  return {data: combinedSeries, yMin: yMin, yMax: yMax};
+};
+
+/**
+ * @private
+ * Places the zoom handles in the proper position based on the current X data window.
+ */
+DygraphRangeSelector.prototype.placeZoomHandles_ = function() {
+  var xExtremes = this.dygraph_.xAxisExtremes();
+  var xWindowLimits = this.dygraph_.xAxisRange();
+  var xRange = xExtremes[1] - xExtremes[0];
+  var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange);
+  var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange);
+  var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent;
+  var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent);
+  var handleTop = Math.round(Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2));
+  var halfHandleWidth = Math.round(this.leftZoomHandle_.width/2);
+  this.leftZoomHandle_.style.left = Math.round(leftCoord - halfHandleWidth) + 'px';
+  this.leftZoomHandle_.style.top = handleTop + 'px';
+  this.rightZoomHandle_.style.left = Math.round(rightCoord - halfHandleWidth) + 'px';
+  this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top;
+
+  this.leftZoomHandle_.style.visibility = 'visible';
+  this.rightZoomHandle_.style.visibility = 'visible';
+};
+
+/**
+ * @private
+ * Draws the interactive layer in the foreground canvas.
+ */
+DygraphRangeSelector.prototype.drawInteractiveLayer_ = function() {
+  var ctx = this.fgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  var margin = 1;
+  var width = this.canvasRect_.w - margin;
+  var height = this.canvasRect_.h - margin;
+  var zoomHandleStatus = this.getZoomHandleStatus_();
+
+  ctx.strokeStyle = 'black';
+  if (!zoomHandleStatus.isZoomed) {
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(margin, height);
+    ctx.lineTo(width, height);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+  } else {
+    leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x);
+    rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x);
+
+    ctx.fillStyle = 'rgba(240, 240, 240, 0.6)';
+    ctx.fillRect(margin, margin, leftHandleCanvasPos, height - margin);
+    ctx.fillRect(rightHandleCanvasPos, margin, width - rightHandleCanvasPos, height - margin);
+
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(leftHandleCanvasPos, margin);
+    ctx.lineTo(leftHandleCanvasPos, height);
+    ctx.lineTo(rightHandleCanvasPos, height);
+    ctx.lineTo(rightHandleCanvasPos, margin);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Returns the current zoom handle position information.
+ * @return {Object} The zoom handle status.
+ */
+DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() {
+  var halfHandleWidth = Math.round(this.leftZoomHandle_.width/2);
+  var leftHandlePos = parseInt(this.leftZoomHandle_.style.left) + halfHandleWidth;
+  var rightHandlePos = parseInt(this.rightZoomHandle_.style.left) + halfHandleWidth;
+  return {
+      leftHandlePos: leftHandlePos,
+      rightHandlePos: rightHandlePos,
+      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
+  };
+};
index cbaaa43..0ca06e7 100644 (file)
@@ -339,7 +339,7 @@ Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
     var year_mod = 1;  // e.g. to only print one point every 10 years.
 
     if (granularity == Dygraph.MONTHLY) {
-      months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
+      months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
     } else if (granularity == Dygraph.QUARTERLY) {
       months = [ 0, 3, 6, 9 ];
     } else if (granularity == Dygraph.BIANNUAL) {
index 9192ae4..1b6a593 100644 (file)
@@ -93,20 +93,35 @@ Dygraph.getContext = function(canvas) {
  * @private
  * Add an event handler. This smooths a difference between IE and the rest of
  * the world.
- * @param { DOM element } el The element to add the event to.
- * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
  * @param { Function } fn The function to call on the event. The function takes
  * one parameter: the event object.
  */
-Dygraph.addEvent = function(el, evt, fn) {
-  var normed_fn = function(e) {
-    if (!e) var e = window.event;
-    fn(e);
-  };
-  if (window.addEventListener) {  // Mozilla, Netscape, Firefox
-    el.addEventListener(evt, normed_fn, false);
-  } else {  // IE
-    el.attachEvent('on' + evt, normed_fn);
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+  if (elem.addEventListener) {
+    elem.addEventListener(type, fn, false);
+  } else {
+    elem[type+fn] = function(){fn(window.event);};
+    elem.attachEvent('on'+type, elem[type+fn]);
+  }
+};
+
+/**
+ * @private
+ * Remove an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.removeEvent = function addEvent(elem, type, fn) {
+  if (elem.removeEventListener) {
+    elem.removeEventListener(type, fn, false);
+  } else {
+    elem.detachEvent('on'+type, elem[type+fn]);
+    elem[type+fn] = null;
   }
 };
 
@@ -609,6 +624,8 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'pixelsPerYLabel': true,
     'pointClickCallback': true,
     'pointSize': true,
+    'rangeSelectorPlotFillColor': true,
+    'rangeSelectorPlotStrokeColor': true,
     'showLabelsOnHighlight': true,
     'showRoller': true,
     'sigFigs': true,
@@ -621,7 +638,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'yAxisLabelFormatter': true,
     'yValueFormatter': true,
     'zoomCallback': true
-  };    
+  };
 
   // Assume that we do not require new points.
   // This will change to true if we actually do need new points.
@@ -659,7 +676,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
       // If this was not a series specific option list, check if its a pixel changing property.
       } else if (!pixelSafeOptions[property]) {
         requiresNewPoints = true;
-      }   
+      }
     }
   }
 
index 66d32fa..8125141 100644 (file)
@@ -244,6 +244,12 @@ Dygraph.DEFAULT_ATTRS = {
 
   interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
 
+  // Range selector options
+  showRangeSelector: false,
+  rangeSelectorHeight: 40,
+  rangeSelectorPlotStrokeColor: "#808FAB",
+  rangeSelectorPlotFillColor: "#A7B1C4",
+
   // per-axis options
   axes: {
     x: {
@@ -305,6 +311,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
       document.readyState != 'complete') {
     var self = this;
     setTimeout(function() { self.__init__(div, file, attrs) }, 100);
+    return;
   }
 
   // Support two-argument constructor
@@ -348,15 +355,15 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   if (div.style.height == '' && attrs.height) {
     div.style.height = attrs.height + "px";
   }
-  if (div.style.height == '' && div.offsetHeight == 0) {
+  if (div.style.height == '' && div.clientHeight == 0) {
     div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
     if (div.style.width == '') {
       div.style.width = Dygraph.DEFAULT_WIDTH + "px";
     }
   }
   // these will be zero if the dygraph's div is hidden.
-  this.width_ = div.offsetWidth;
-  this.height_ = div.offsetHeight;
+  this.width_ = div.clientWidth;
+  this.height_ = div.clientHeight;
 
   // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
   if (attrs['stackedGraph']) {
@@ -779,11 +786,26 @@ Dygraph.prototype.createInterface_ = function() {
   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
   this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
 
+  if (this.attr_('showRangeSelector')) {
+    // The range selector must be created here so that its canvases and contexts get created here.
+    // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
+    // The range selector also sets xAxisHeight in order to reserve space.
+    this.rangeSelector_ = new DygraphRangeSelector(this);
+  }
+
   // The interactive parts of the graph are drawn on top of the chart.
   this.graphDiv.appendChild(this.hidden_);
   this.graphDiv.appendChild(this.canvas_);
   this.mouseEventElement_ = this.canvas_;
 
+  // Create the grapher
+  this.layout_ = new DygraphLayout(this);
+
+  if (this.rangeSelector_) {
+    // This needs to happen after the graph canvases are added to the div and the layout object is created.
+    this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
+  }
+
   var dygraph = this;
   Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
     dygraph.mouseMove_(e);
@@ -792,9 +814,6 @@ Dygraph.prototype.createInterface_ = function() {
     dygraph.mouseOut_(e);
   });
 
-  // Create the grapher
-  this.layout_ = new DygraphLayout(this);
-
   this.createStatusMessage_();
   this.createDragInterface_();
 
@@ -1106,7 +1125,7 @@ Dygraph.prototype.createDragInterface_ = function() {
  * up any previous zoom rectangles that were drawn. This could be optimized to
  * avoid extra redrawing, but it's tricky to avoid interactions with the status
  * dots.
- * 
+ *
  * @param {Number} direction the direction of the zoom rectangle. Acceptable
  * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
  * @param {Number} startX The X position where the drag started, in canvas
@@ -1492,11 +1511,11 @@ Dygraph.prototype.setSelection = function(row) {
     for (var i in this.layout_.datasets) {
       if (row < this.layout_.datasets[i].length) {
         var point = this.layout_.points[pos+row];
-        
+
         if (this.attr_("stackedGraph")) {
           point = this.layout_.unstackPointAtIndex(pos+row);
         }
-        
+
         this.selPoints_.push(point);
       }
       pos += this.layout_.datasets[i].length;
@@ -1665,6 +1684,10 @@ Dygraph.prototype.predraw_ = function() {
   // edge of the div, if we have two y-axes.
   this.positionLabelsDiv_();
 
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderStaticLayer();
+  }
+
   // If the data or options have changed, then we'd better redraw.
   this.drawGraph_();
 
@@ -1857,6 +1880,10 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
     }
   }
 
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderInteractiveLayer();
+  }
+
   if (this.attr_("drawCallback") !== null) {
     this.attr_("drawCallback")(this, is_initial_draw);
   }
@@ -2824,8 +2851,8 @@ Dygraph.prototype.resize = function(width, height) {
     this.width_ = width;
     this.height_ = height;
   } else {
-    this.width_ = this.maindiv_.offsetWidth;
-    this.height_ = this.maindiv_.offsetHeight;
+    this.width_ = this.maindiv_.clientWidth;
+    this.height_ = this.maindiv_.clientHeight;
   }
 
   if (old_width != this.width_ || old_height != this.height_) {
index 0f3064a..ea9f92b 100755 (executable)
@@ -12,6 +12,7 @@ dygraph.js \
 dygraph-utils.js \
 dygraph-gviz.js \
 dygraph-interaction-model.js \
+dygraph-range-selector.js \
 dygraph-tickers.js \
 rgbcolor/rgbcolor.js \
 strftime/strftime-min.js \
index 7d89afd..5f6d1fa 100644 (file)
@@ -12,6 +12,7 @@ load:
   - dygraph-gviz.js
   - dygraph-interaction-model.js
   - dygraph-options-reference.js
+  - dygraph-range-selector.js
   - dygraph-tickers.js
   - dygraph-dev.js
   - excanvas.js
diff --git a/tests/range-selector.html b/tests/range-selector.html
new file mode 100644 (file)
index 0000000..58af000
--- /dev/null
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Temperatures with Range Selector</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <!--
+    For production (minified) code, use:
+    <script type="text/javascript" src="dygraph-combined.js"></script>
+    -->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+    <script type="text/javascript" src="data.js"></script>
+    <style type="text/css">
+    #bordered {
+      border: 1px solid red;
+    }
+    </style>
+  </head>
+  <body>
+    <p>Demo of a graph with the range selector.</p>
+    <p>
+      No roll period.
+    </p>
+    <div id="noroll" style="width:800px; height:320px;"></div>
+    <p>
+      Roll period of 14 timesteps, custom range selector height and plot color.
+    </p>
+    <div id="roll14" style="width:800px; height:320px;"></div>
+    <script type="text/javascript">
+      g1 = new Dygraph(
+          document.getElementById("noroll"),
+          data_temp,
+          {
+            customBars: true,
+            title: 'Daily Temperatures in New York vs. San Francisco',
+            ylabel: 'Temperature (F)',
+            legend: 'always',
+            labelsDivStyles: { 'textAlign': 'right' },
+            showRangeSelector: true
+          }
+      );
+      g2 = new Dygraph(
+          document.getElementById("roll14"),
+          data_temp,
+          {
+            rollPeriod: 14,
+            showRoller: true,
+            customBars: true,
+            title: 'Daily Temperatures in New York vs. San Francisco',
+            ylabel: 'Temperature (F)',
+            legend: 'always',
+            labelsDivStyles: { 'textAlign': 'right' },
+            showRangeSelector: true,
+            rangeSelectorHeight: 30,
+            rangeSelectorPlotStrokeColor: 'yellow',
+            rangeSelectorPlotFillColor: 'lightyellow'
+          }
+      );
+    </script>
+  </body>
+</html>