Merge branch 'master' of https://github.com/nealie/dygraphs into nealie
authorDan Vanderkam <dan@dygraphs.com>
Mon, 4 Apr 2011 23:51:53 +0000 (19:51 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Mon, 4 Apr 2011 23:51:53 +0000 (19:51 -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 4432498..30ea7a8 100644 (file)
@@ -258,6 +258,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;
@@ -334,6 +338,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
@@ -1495,6 +1515,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());
@@ -1522,9 +1543,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());
   }
 };
@@ -1552,6 +1575,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];
@@ -2576,15 +2601,20 @@ Dygraph.prototype.drawGraph_ = function() {
     this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
   }
 
-  this.computeYAxisRanges_(extremes);
-  this.layout_.updateOptions( { yAxes: this.axes_,
-                                seriesToAxisMap: this.seriesToAxisMap_
-                              } );
-
+  if (datasets.length > 0) {
+    // TODO(danvk): this method doesn't need to return anything.
+    this.computeYAxisRanges_(extremes);
+    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();
@@ -2612,6 +2642,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_ = {};
 
@@ -2688,6 +2727,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];
+    }
+  }
 };
 
 /**
@@ -2742,12 +2788,29 @@ 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.
@@ -3469,6 +3532,12 @@ Dygraph.prototype.start_ = function() {
  * <li>file: changes the source data for the graph</li>
  * <li>errorBars: changes whether the data contains stddev</li>
  * </ul>
+ *
+ * If the dateWindow or valueRange options are specified, the relevant zoomed_x_
+ * or zoomed_y_ flags are set, unless the isZoomedIgnoreProgrammaticZoom option is also
+ * secified. This allows for the chart to be programmatically zoomed without
+ * altering the zoomed flags.
+ *
  * @param {Object} attrs The new properties and values
  */
 Dygraph.prototype.updateOptions = function(attrs) {
@@ -3478,6 +3547,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.
@@ -4135,6 +4210,12 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "type": "float",
     "default": "null",
     "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
+  },
+  "isZoomedIgnoreProgrammaticZoom" : {
+    "default": "false",
+    "labels": ["Zooming"],
+    "type": "boolean",
+    "description" : "When this flag is passed 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>
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..7b2629e
--- /dev/null
@@ -0,0 +1,167 @@
+<html>
+    <head>
+        <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..50c23f1
--- /dev/null
@@ -0,0 +1,103 @@
+<html>
+    <head>
+        <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");
               }
             }
           );