merge master
authorDan Vanderkam <dan@dygraphs.com>
Tue, 5 Apr 2011 14:35:42 +0000 (10:35 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Tue, 5 Apr 2011 14:35:42 +0000 (10:35 -0400)
dygraph.js
tests/is-zoomed-ignore-programmatic-zoom.html [new file with mode: 0644]
tests/is-zoomed.html [new file with mode: 0644]
tests/zoom.html

index 88ad4b8..fe677f2 100644 (file)
@@ -263,6 +263,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.is_initial_draw_ = true;
   this.annotations_ = [];
 
+  // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
+  this.zoomed_x_ = false;
+  this.zoomed_y_ = false;
+
   // Number of digits to use when labeling the x (if numeric) and y axis
   // ticks.
   this.numXDigits_ = 2;
@@ -339,6 +343,22 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.start_();
 };
 
+/**
+ * Returns the zoomed status of the chart for one or both axes.
+ *
+ * Axis is an optional parameter. Can be set to 'x' or 'y'.
+ *
+ * The zoomed status for an axis is set whenever a user zooms using the mouse
+ * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
+ * option is also specified).
+ */
+Dygraph.prototype.isZoomed = function(axis) {
+  if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
+  if (axis == 'x') return this.zoomed_x_;
+  if (axis == 'y') return this.zoomed_y_;
+  throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
+};
+
 Dygraph.prototype.toString = function() {
   var maindiv = this.maindiv_;
   var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
@@ -1503,6 +1523,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) {
  */
 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
   this.dateWindow_ = [minDate, maxDate];
+  this.zoomed_x_ = true;
   this.drawGraph_();
   if (this.attr_("zoomCallback")) {
     this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
@@ -1530,9 +1551,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) {
     valueRanges.push([low, hi]);
   }
 
+  this.zoomed_y_ = true;
   this.drawGraph_();
   if (this.attr_("zoomCallback")) {
     var xRange = this.xAxisRange();
+    var yRange = this.yAxisRange();
     this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
   }
 };
@@ -1560,6 +1583,8 @@ Dygraph.prototype.doUnzoom_ = function() {
   if (dirty) {
     // Putting the drawing operation before the callback because it resets
     // yAxisRange.
+    this.zoomed_x_ = false;
+    this.zoomed_y_ = false;
     this.drawGraph_();
     if (this.attr_("zoomCallback")) {
       var minDate = this.rawData_[0][0];
@@ -2588,11 +2613,13 @@ Dygraph.prototype.drawGraph_ = function() {
   this.layout_.updateOptions( { yAxes: this.axes_,
                                 seriesToAxisMap: this.seriesToAxisMap_
                               } );
-
   this.addXTicks_();
 
+  // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
+  var tmp_zoomed_x = this.zoomed_x_;
   // Tell PlotKit to use this new data and render itself
   this.layout_.updateOptions({dateWindow: this.dateWindow_});
+  this.zoomed_x_ = tmp_zoomed_x;
   this.layout_.evaluateWithError();
   this.plotter_.clear();
   this.plotter_.render();
@@ -2620,6 +2647,15 @@ Dygraph.prototype.drawGraph_ = function() {
  *   indices are into the axes_ array.
  */
 Dygraph.prototype.computeYAxes_ = function() {
+  var valueWindows;
+  if (this.axes_ != undefined) {
+    // Preserve valueWindow settings.
+    valueWindows = [];
+    for (var index = 0; index < this.axes_.length; index++) {
+      valueWindows.push(this.axes_[index].valueWindow);
+    }
+  }
+
   this.axes_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
   this.seriesToAxisMap_ = {};
 
@@ -2696,6 +2732,13 @@ Dygraph.prototype.computeYAxes_ = function() {
     if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
   }
   this.seriesToAxisMap_ = seriesToAxisFiltered;
+
+  if (valueWindows != undefined) {
+    // Restore valueWindow settings.
+    for (var index = 0; index < valueWindows.length; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
 };
 
 /**
@@ -2750,12 +2793,24 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
       var series = seriesForAxis[i];
       var minY = Infinity;  // extremes[series[0]][0];
       var maxY = -Infinity;  // extremes[series[0]][1];
+      var extremeMinY, extremeMaxY;
       for (var j = 0; j < series.length; j++) {
-        minY = Math.min(extremes[series[j]][0], minY);
-        maxY = Math.max(extremes[series[j]][1], maxY);
+        // Only use valid extremes to stop null data series' from corrupting the scale.
+        extremeMinY = extremes[series[j]][0];
+        if (extremeMinY != null) {
+          minY = Math.min(extremeMinY, minY);
+        }
+        extremeMaxY = extremes[series[j]][1];
+        if (extremeMaxY != null) {
+          maxY = Math.max(extremeMaxY, maxY);
+        }
       }
       if (axis.includeZero && minY > 0) minY = 0;
 
+      // Ensure we have a valid scale, otherwise defualt to zero for safety.
+      if (minY == Infinity) minY = 0;
+      if (maxY == -Infinity) maxY = 0;
+
       // Add some padding and round up to an integer to be human-friendly.
       var span = maxY - minY;
       // special case: if we have no sense of scale, use +/-10% of the sole value.
@@ -3477,6 +3532,7 @@ Dygraph.prototype.start_ = function() {
  * <li>file: changes the source data for the graph</li>
  * <li>errorBars: changes whether the data contains stddev</li>
  * </ul>
+ *
  * @param {Object} attrs The new properties and values
  */
 Dygraph.prototype.updateOptions = function(attrs) {
@@ -3486,6 +3542,12 @@ Dygraph.prototype.updateOptions = function(attrs) {
   }
   if ('dateWindow' in attrs) {
     this.dateWindow_ = attrs.dateWindow;
+    if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+      this.zoomed_x_ = attrs.dateWindow != null;
+    }
+  }
+  if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+    this.zoomed_y_ = attrs.valueRange != null;
   }
 
   // TODO(danvk): validate per-series options.
@@ -4179,6 +4241,12 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "type": "integer",
     "default": "18",
     "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
+  },
+  "isZoomedIgnoreProgrammaticZoom" : {
+    "default": "false",
+    "labels": ["Zooming"],
+    "type": "boolean",
+    "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
   }
 }
 ;  // </JSON>
@@ -4205,7 +4273,8 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
    'Legend',
    'Overall display',
    'Rolling Averages',
-   'Value display/formatting'
+   'Value display/formatting',
+   'Zooming'
   ];
   var cats = {};
   for (var i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
diff --git a/tests/is-zoomed-ignore-programmatic-zoom.html b/tests/is-zoomed-ignore-programmatic-zoom.html
new file mode 100644 (file)
index 0000000..e8ae826
--- /dev/null
@@ -0,0 +1,168 @@
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>isZoomedIgnoreProgrammaticZoom Flag</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+  </head>
+  <body>
+    <!-- Ensure that the documentation generator picks us up: {isZoomedIgnoreProgrammaticZoom:} -->
+    <h1>isZoomedIgnoreProgrammaticZoom Option</h1>
+    <p>
+      By default, when the <code>dateWindow</code> or <code>updateOptions</code>
+      of a chart is changed programmatically by a call to <code>updateOptions</code>
+      the zoomed flags (<code>isZoomed</code>) are changed. This is the same
+      as manually zooming in using the mouse.
+    </p>
+    <p>
+      Sometimes it may be desirable to change the display of the chart by
+      manipulating the <code>dateWindow</code> and <code>valueRange</code>
+      options but without changing the zoomed flags, for example where manual
+      zooming is still required but where it is also desired that the zoomed
+      flags drive display elements, but only for manual zooming.
+    </p>
+    <p>
+      In this case <code>isZoomedIgnoreProgrammaticZoom</code> may be specified along with
+      either the <code>dateWindow</code> or <code>valueRange</code> values to
+      <code>updateOptions</code> and the zoomed flags will remain unaffected.
+    </p>
+    <p>
+      The chart below may be manipulated to change the <code>updateOptions</code>
+      using the Max and Min Y axis buttons and the <code>dateWindow</code>
+      by using the Max and Min X axis buttons.
+    </p>
+    <p>
+      Toggle the check box below to determine the difference in operation of the zoom flags
+      when the date and value windows of the chart are changed using the arrows underneath.
+    </p>
+    <p><input id="isZoomedIgnoreProgrammaticZoom" type="checkbox" checked=true />Do not change zoom flags (<code>isZoomedIgnoreProgrammaticZoom</code>)</p>
+
+    <div>
+      <div style="float: left">
+        <p>
+          Max Y Axis:
+          <input type="button" value="&uarr;" onclick="adjustTop(+1)" />
+          <input type="button" value="&darr;" onclick="adjustTop(-1)" />
+        </p>
+        <p>
+          Min Y Axis:
+          <input type="button" value="&uarr;" onclick="adjustBottom(+1)" />
+          <input type="button" value="&darr;" onclick="adjustBottom(-1)" />
+        </p>
+        <p>
+          Min X Axis:
+          <input type="button" value="&larr;" onclick="adjustFirst(-100000000)" />
+          <input type="button" value="&rarr;" onclick="adjustFirst(+100000000)" />
+        </p>
+        <p>
+          Max X Axis:
+          <input type="button" value="&larr;" onclick="adjustLast(-100000000)" />
+          <input type="button" value="&rarr;" onclick="adjustLast(+100000000)" />
+        </p>
+      </div>
+      <div id="div_g" style="width: 600px; height: 300px; float: left"></div>
+      <div style="float: left">
+
+      </div>
+    </div>
+    <div style="display: inline-block">
+      <h4> Zoomed Flags</h4>
+      <p>Zoomed: <span id="zoomed">False</span></p>
+      <p>Zoomed X: <span id="zoomedX">False</span></p>
+      <p>Zoomed Y: <span id="zoomedY">False</span></p>
+      <h4>Window coordinates (in dates and values):</h4>
+      <div id="xdimensions"></div>
+      <div id="ydimensions"></div>
+    </div>
+
+    <script type="text/javascript">
+      g = new Dygraph(
+        document.getElementById("div_g"),
+        NoisyData,
+        {
+          errorBars: true,
+          zoomCallback : function(minDate, maxDate, yRange) {
+            showDimensions(minDate, maxDate, yRange);
+          },
+          drawCallback: function(me, initial) {
+            document.getElementById("zoomed").innerHTML = "" + me.isZoomed();
+            document.getElementById("zoomedX").innerHTML = "" + me.isZoomed("x");
+            document.getElementById("zoomedY").innerHTML = "" + me.isZoomed("y");
+            var x_range = me.xAxisRange()
+            var elem = document.getElementById("xdimensions")
+            elem.innerHTML = "dateWindow : [" + x_range[0] + ", "+ x_range[1] + "]"
+          }
+        }
+      )
+
+      // Pull an initial value for logging.
+      var minDate = g.xAxisRange()[0];
+      var maxDate = g.xAxisRange()[1];
+      var minValue = g.yAxisRange()[0];
+      var maxValue = g.yAxisRange()[1];
+      showDimensions(minDate, maxDate, [minValue, maxValue]);
+
+      function showDimensions(minDate, maxDate, yRanges) {
+        showXDimensions(minDate, maxDate);
+        showYDimensions(yRanges);
+      }
+
+      function getNoChange() {
+        var options = {}
+        var elem = document.getElementById("isZoomedIgnoreProgrammaticZoom")
+        if (elem.checked) {
+          options.isZoomedIgnoreProgrammaticZoom = true
+        }
+        return options
+      }
+
+      function adjustTop(value) {
+        options = getNoChange()
+        maxValue += value
+        options.valueRange = [minValue, maxValue]
+        console.log(options)
+        g.updateOptions(options)
+      }
+
+      function adjustBottom(value) {
+        options = getNoChange()
+        minValue += value
+        options.valueRange = [minValue, maxValue]
+        console.log(options)
+        g.updateOptions(options)
+      }
+
+      function adjustFirst(value) {
+        options = getNoChange()
+        minDate += value
+        options.dateWindow = [minDate, maxDate]
+        console.log(options)
+        g.updateOptions(options)
+      }
+
+      function adjustLast(value) {
+        options = getNoChange()
+        maxDate += value
+        options.dateWindow = [minDate, maxDate]
+        g.updateOptions(options)
+      }
+
+      function showXDimensions(first, second) {
+        var elem = document.getElementById("xdimensions");
+        elem.innerHTML = "dateWindow: [" + first + ", "+ second + "]";
+      }
+
+      function showYDimensions(ranges) {
+        var elem = document.getElementById("ydimensions");
+        elem.innerHTML = "valueRange: [" + ranges + "]";
+      }
+
+    </script>
+  </body>
+</html>
diff --git a/tests/is-zoomed.html b/tests/is-zoomed.html
new file mode 100644 (file)
index 0000000..1b5db9f
--- /dev/null
@@ -0,0 +1,104 @@
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>isZoomedIgnoresProgrammaticZoom Flag</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+  </head>
+  <body>
+    <h1 id="zoom">Determining Zoom</h1>
+    <p>
+      It is possible to detect whether a chart has been zoomed in either axis by the use of the <code>isZoomed</code> function.
+      If called with no argument, it will report whether <em>either</em> axis has been zoomed.
+      Alternatively it can be called with an argument of either <code>'x'</code> or <code>'y'</code> and it will report the status of just that axis.
+    </p>
+  
+    <p>Here's a simple example using <code>drawCallback</code> to display the various zoom states whenever the chart is zoomed:</p>
+  
+    <div style="width:600px; text-align:center; font-weight: bold; font-size: 125%;">OUTPUT</div>
+    <div style="width: 750px">
+      <div style="float: right">
+        <p>Zoomed: <span id="zoomed">False</span><p/>
+        <p>Zoomed X: <span id="zoomedX">False</span><p/>
+        <p>Zoomed Y: <span id="zoomedY">False</span><p/>
+      </div>
+      <div class="codeoutput" style="float:left;">
+      <div id="zoomdiv"></div>
+      <script type="text/javascript">
+        new Dygraph(
+  
+        // containing div
+        document.getElementById("zoomdiv"),
+  
+        // CSV or path to a CSV file.
+        "Date,Value\n" +
+        "2011-01-07,75\n" +
+        "2011-01-08,70\n" +
+        "2011-01-09,90\n" +
+        "2011-01-10,30\n" +
+        "2011-01-11,40\n" +
+        "2011-01-12,60\n" +
+        "2011-01-13,70\n" +
+        "2011-01-14,40\n",
+        {
+          drawCallback: function(me, initial) {
+          document.getElementById("zoomed").innerHTML = "" + me.isZoomed();
+          document.getElementById("zoomedX").innerHTML = "" + me.isZoomed("x");
+          document.getElementById("zoomedY").innerHTML = "" + me.isZoomed("y");
+          }
+        }
+        );
+      </script>
+      </div>
+    </div>
+  
+    <p>
+      <div style="clear:both; width:600px; text-align:center; font-weight: bold; font-size: 125%;">HTML</div>
+
+<pre>
+  new Dygraph(
+
+  // containing div
+  document.getElementById(&quot;zoomdiv&quot;),
+
+  // CSV or path to a CSV file.
+  &quot;Date,Temperature\n&quot; +
+  &quot;2011-01-07,75\n&quot; +
+  &quot;2011-01-08,70\n&quot; +
+  &quot;2011-01-09,90\n&quot; +
+  &quot;2011-01-10,30\n&quot; +
+  &quot;2011-01-11,40\n&quot; +
+  &quot;2011-01-12,60\n&quot; +
+  &quot;2011-01-13,70\n&quot; +
+  &quot;2011-01-14,40\n&quot;,
+  {
+    drawCallback: function(me, initial) {
+    document.getElementById(&quot;zoomed&quot;).innerHTML = &quot;&quot; + me.isZoomed();
+    document.getElementById(&quot;zoomedX&quot;).innerHTML = &quot;&quot; + me.isZoomed(&quot;x&quot;);
+    document.getElementById(&quot;zoomedY&quot;).innerHTML = &quot;&quot; + me.isZoomed(&quot;y&quot;);
+    }
+  }
+  );
+</pre>
+    </p>
+  
+    <p>The <a href="tests/zoom.html">Tests for zoom operations</a> show a full example of this in action.</p>
+  
+    <h3>Programmatic Zoom</h3>
+    <p>
+      When a chart is programmatically zoomed by updating either the <code>dateWindow</code>
+      or <code>valueRange</code> option, by default the zoomed flags are also updated correspondingly.
+      It is possible to prevent this by specifying the <code>isZoomedIgnoreProgrammaticZoom</code> in the same
+      call to the <code>updateOptions</code> method.
+    </p>
+    <p>
+      The <a href="tests/is-zoomed-ignore-programmatic-zoom.html">is-zoomed-ignore-programmatic-zoom</a> test shows this in operation.
+    </p>
+  </body>
+</html>
index 65f23a4..286e1b3 100644 (file)
     buttons are useful for testing.</h3>
     <h4>Window coordinates (in dates and values):</h4>
     <div id="xdimensions"></div> <div id="ydimensions"></div>
+    <div style="float: right">
+        <p>Zoomed: <span id="zoomed">False</span><p/>
+        <p>Zoomed X: <span id="zoomedX">False</span><p/>
+        <p>Zoomed Y: <span id="zoomedY">False</span><p/>
+    </div>
     <div id="div_g" style="width:600px; height:300px;"></div>
 
+
     <p><b>Zoom operations:</b></p>
     <p>
       <input type="button" value="Y (3,5)" onclick="zoomGraphY(3,5)">&nbsp;
             document.getElementById("div_g"),
             NoisyData, {
               errorBars: true,
-              zoomCallback : function(minDate, maxDate, yRanges) {
-                showDimensions(minDate, maxDate, yRanges);
+              zoomCallback : function(minDate, maxDate, yRange) {
+                showDimensions(minDate, maxDate, yRange);
+              },
+              drawCallback: function(me, initial) {
+                document.getElementById("zoomed").innerHTML = "" + me.isZoomed();
+                document.getElementById("zoomedX").innerHTML = "" + me.isZoomed("x");
+                document.getElementById("zoomedY").innerHTML = "" + me.isZoomed("y");
               }
             }
           );