Merge branch 'master' into chart-labels
authorDan Vanderkam <dan@dygraphs.com>
Thu, 31 Mar 2011 16:31:57 +0000 (12:31 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Thu, 31 Mar 2011 16:31:57 +0000 (12:31 -0400)
dygraph.js
generate-documentation.py
tests/dygraph-many-points-benchmark.html
tests/per-series.html
tests/perf.html
tests/zoom.html

index f5a2ce2..ec4ce54 100644 (file)
@@ -414,9 +414,14 @@ Dygraph.prototype.rollPeriod = function() {
  * If the Dygraph has dates on the x-axis, these will be millis since epoch.
  */
 Dygraph.prototype.xAxisRange = function() {
-  if (this.dateWindow_) return this.dateWindow_;
+  return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
+};
 
-  // The entire chart is visible.
+/**
+ * Returns the lower- and upper-bound x-axis values of the
+ * data set.
+ */
+Dygraph.prototype.xAxisExtremes = function() {
   var left = this.rawData_[0][0];
   var right = this.rawData_[this.rawData_.length - 1][0];
   return [left, right];
@@ -569,7 +574,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) {
 
 /**
  * Converts a y for an axis to a percentage from the top to the
- * bottom of the div.
+ * bottom of the drawing area.
  *
  * If the coordinate represents a value visible on the canvas, then
  * the value will be between 0 and 1, where 0 is the top of the canvas.
@@ -590,8 +595,8 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) {
 
   var pct;
   if (!this.axes_[axis].logscale) {
-    // yrange[1] - y is unit distance from the bottom.
-    // yrange[1] - yrange[0] is the scale of the range.
+    // yRange[1] - y is unit distance from the bottom.
+    // yRange[1] - yRange[0] is the scale of the range.
     // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
     pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
   } else {
@@ -602,6 +607,26 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) {
 }
 
 /**
+ * Converts an x value to a percentage from the left to the right of
+ * the drawing area.
+ *
+ * If the coordinate represents a value visible on the canvas, then
+ * the value will be between 0 and 1, where 0 is the left of the canvas.
+ * However, this method will return values outside the range, as
+ * values can fall outside the canvas.
+ *
+ * If x is null, this returns null.
+ */
+Dygraph.prototype.toPercentXCoord = function(x) {
+  if (x == null) {
+    return null;
+  }
+
+  var xRange = this.xAxisRange();
+  return (x - xRange[0]) / (xRange[1] - xRange[0]);
+}
+
+/**
  * Returns the number of columns (including the independent variable).
  */
 Dygraph.prototype.numColumns = function() {
@@ -1014,6 +1039,35 @@ Dygraph.startPan = function(event, g, context) {
   context.initialLeftmostDate = xRange[0];
   context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
 
+  if (g.attr_("panEdgeFraction")) {
+    var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
+    var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
+
+    var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
+    var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
+
+    var boundedLeftDate = g.toDataXCoord(boundedLeftX);
+    var boundedRightDate = g.toDataXCoord(boundedRightX);
+    context.boundedDates = [boundedLeftDate, boundedRightDate];
+
+    var boundedValues = [];
+    var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
+
+    for (var i = 0; i < g.axes_.length; i++) {
+      var axis = g.axes_[i];
+      var yExtremes = axis.extremeRange;
+
+      var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
+      var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
+
+      var boundedTopValue = g.toDataYCoord(boundedTopY);
+      var boundedBottomValue = g.toDataYCoord(boundedBottomY);
+
+      boundedValues[i] = [boundedTopValue, boundedBottomValue];
+    }
+    context.boundedValues = boundedValues;
+  }
+
   // Record the range of each y-axis at the start of the drag.
   // If any axis has a valueRange or valueWindow, then we want a 2D pan.
   context.is2DPan = false;
@@ -1049,7 +1103,18 @@ Dygraph.movePan = function(event, g, context) {
 
   var minDate = context.initialLeftmostDate -
     (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
+  if (context.boundedDates) {
+    minDate = Math.max(minDate, context.boundedDates[0]);
+  }
   var maxDate = minDate + context.dateRange;
+  if (context.boundedDates) {
+    if (maxDate > context.boundedDates[1]) {
+      // Adjust minDate, and recompute maxDate.
+      minDate = minDate - (maxDate - context.boundedDates[1]);
+      maxDate = minDate + context.dateRange;
+    }
+  }
+
   g.dateWindow_ = [minDate, maxDate];
 
   // y-axis scaling is automatic unless this is a full 2D pan.
@@ -1060,10 +1125,22 @@ Dygraph.movePan = function(event, g, context) {
 
       var pixelsDragged = context.dragEndY - context.dragStartY;
       var unitsDragged = pixelsDragged * axis.unitsPerPixel;
+      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
 
       // In log scale, maxValue and minValue are the logs of those values.
       var maxValue = axis.initialTopValue + unitsDragged;
+      if (boundedValue) {
+        maxValue = Math.min(maxValue, boundedValue[1]);
+      }
       var minValue = maxValue - axis.dragValueRange;
+      if (boundedValue) {
+        if (minValue < boundedValue[0]) {
+          // Adjust maxValue, and recompute minValue.
+          maxValue = maxValue - (minValue - boundedValue[0]);
+          minValue = maxValue - axis.dragValueRange;
+        }
+      }
       if (axis.logscale) {
         axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
                              Math.pow(Dygraph.LOG_SCALE, maxValue) ];
@@ -1092,6 +1169,8 @@ Dygraph.endPan = function(event, g, context) {
   context.initialLeftmostDate = null;
   context.dateRange = null;
   context.valueRange = null;
+  context.boundedDates = null;
+  context.boundedValues = null;
 }
 
 // Called in response to an interaction model operation that
@@ -1281,6 +1360,11 @@ Dygraph.prototype.createDragInterface_ = function() {
     px: 0,
     py: 0,
 
+    // Values for use with panEdgeFraction, which limit how far outside the
+    // graph's data boundaries it can be panned.
+    boundedDates: null, // [minDate, maxDate]
+    boundedValues: null, // [[minValue, maxValue] ...]
+
     initializeMouseDown: function(event, g, context) {
       // prevents mouse drags from selecting page text.
       if (event.preventDefault) {
@@ -1493,12 +1577,12 @@ Dygraph.prototype.doUnzoom_ = function() {
  * @private
  */
 Dygraph.prototype.mouseMove_ = function(event) {
-  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
-  var points = this.layout_.points;
-
   // This prevents JS errors when mousing over the canvas before data loads.
+  var points = this.layout_.points;
   if (points === undefined) return;
 
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+
   var lastx = -1;
   var lasty = -1;
 
@@ -2660,15 +2744,8 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
   // Compute extreme values, a span and tick marks for each axis.
   for (var i = 0; i < this.axes_.length; i++) {
     var axis = this.axes_[i];
-    if (axis.valueWindow) {
-      // This is only set if the user has zoomed on the y-axis. It is never set
-      // by a user. It takes precedence over axis.valueRange because, if you set
-      // valueRange, you'd still expect to be able to pan.
-      axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
-    } else if (axis.valueRange) {
-      // This is a user-set value range for this axis.
-      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
-    } else {
+    {
       // Calculate the extremes of extremes.
       var series = seriesForAxis[i];
       var minY = Infinity;  // extremes[series[0]][0];
@@ -2704,8 +2781,18 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
           if (minY > 0) minAxisY = 0;
         }
       }
-
-      axis.computedValueRange = [minAxisY, maxAxisY];
+      axis.extremeRange = [minAxisY, maxAxisY];
+    }
+    if (axis.valueWindow) {
+      // This is only set if the user has zoomed on the y-axis. It is never set
+      // by a user. It takes precedence over axis.valueRange because, if you set
+      // valueRange, you'd still expect to be able to pan.
+      axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
+    } else if (axis.valueRange) {
+      // This is a user-set value range for this axis.
+      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
+    } else {
+      axis.computedValueRange = axis.extremeRange;
     }
 
     // Add ticks. By default, all axes inherit the tick positions of the
@@ -4042,6 +4129,13 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "labels": ["Annotations"],
     "type": "boolean",
     "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
+  },
+  "panEdgeFraction": {
+    "default": "null",
+    "labels": ["Axis Display", "Interactive Elements"],
+    "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."
   }
 }
 ;  // </JSON>
index 155393b..f0b7387 100755 (executable)
@@ -2,6 +2,12 @@
 import json
 import glob
 import re
+import sys
+
+# Set this to the path to a test file to get debug output for just that test
+# file. Can be helpful to figure out why a test is not being shown for a
+# particular option.
+debug_tests = []  # [ 'tests/zoom.html' ]
 
 # Pull options reference JSON out of dygraph.js
 js = ''
@@ -40,14 +46,21 @@ def find_braces(txt):
 
 # Find text followed by a colon. These won't all be options, but those that
 # have the same name as a Dygraph option probably will be.
-prop_re = re.compile(r'\b([a-zA-Z0-9]+):')
-for test_file in glob.glob('tests/*.html'):
+prop_re = re.compile(r'\b([a-zA-Z0-9]+) *:')
+tests = debug_tests or glob.glob('tests/*.html')
+for test_file in tests:
   braced_html = find_braces(file(test_file).read())
+  if debug_tests:
+    print braced_html
+
   ms = re.findall(prop_re, braced_html)
   for opt in ms:
+    if debug_tests: print '\n'.join(ms)
     if opt in docs and test_file not in docs[opt]['tests']:
       docs[opt]['tests'].append(test_file)
 
+if debug_tests: sys.exit(0)
+
 # Extract a labels list.
 labels = []
 for nu, opt in docs.iteritems():
index 77d4537..29d80cf 100644 (file)
     <p>Number of points:
        <input type="text" id="num_points_input" size="20"
               onchange="updatePlot();"></p>
+    <p>Number of series:
+       <input type="text" id="num_series_input" size="20"
+              onchange="updatePlot();"></p>
     <p>Roll period (in points):
       <input type="text" id="roll_period_input" size="20"
               onchange="updatePlot();"></p>
     <br>
     <br>
     <div id="plot"></div>
+    <div id="message"></div>
 
     <script type="text/javascript">
       var plot;
 
       updatePlot = function() {
+        document.getElementById('message').innerHTML = "";
         var plotDiv = document.getElementById('plot');
         plotDiv.innerHTML = 'Redrawing...';
         var numPoints =
             parseInt(document.getElementById('num_points_input').value);
+        var numSeries =
+            parseInt(document.getElementById('num_series_input').value);
+
         var data = [];
         var xmin = 0.0;
         var xmax = 2.0 * Math.PI;
+        var adj = .5;
         var delta = (xmax - xmin) / (numPoints - 1);
 
         for (var i = 0; i < numPoints; ++i) {
           var x = xmin + delta * i;
-          var y = Math.sin(x);
-          data[i] = [x, y];
+          var elem = [ x ];
+          for (var j = 0; j < numSeries; j++) {
+            var y = Math.sin(x + (j * adj));
+            elem.push(y);
+          }
+          data[i] = elem;
+        }
+        var labels = [ "x" ];
+        for (var j = 0; j < numSeries; j++) {
+          labels.push("sin(x + " + (j*adj) + ")");
         }
-
         var rollPeriod = parseInt(
             document.getElementById('roll_period_input').value);
-        var opts = {labels: ['x', 'sin(x)'], rollPeriod: rollPeriod};
+        var opts = {labels: labels, rollPeriod: rollPeriod};
+        var start = new Date();
         plot = new Dygraph(plotDiv, data, opts);
+        var end = new Date();
+        document.getElementById('message').innerHTML =
+            "completed in " + (end - start) + " milliseconds.";
       };
 
       document.getElementById('num_points_input').value = '100';
+      document.getElementById('num_series_input').value = '1';
       document.getElementById('roll_period_input').value = '1';
       updatePlot();
     </script>
index 75a187c..9fb02d1 100644 (file)
@@ -14,8 +14,6 @@
   <body>
     <h2>Chart with per-series properties</h2>
     <div id="demodiv"></div>
-    <br/><br/>
-    <div id="demodiv2"></div>
 
     <script type="text/javascript">
       g = new Dygraph(
index 7c33a95..4cd5bc0 100644 (file)
     </script>
 
     <p><b>Some numbers on a MacBook Pro 2.53 GHz Core 2 Duo</b><br/>
-    commit bb5899c56e33716db724cb60a5120b91f5fccdeb<br/>
-    Firefox 3.0.15: 28 ms/instantiation<br/>
-    Safari 4.0.3: 15.02 ms/instantiation<Br/>
-    <br/>
-
-    commit 2847c1cf1a2874e9fe56b5749e6e105e37bb086a<br/>
-    Firefox 3.0.15: 49.27 ms/instantiation<br/>
-    Safari 4.0.3: 24.48 ms/instantiation<br/>
+    <table border="1"><tbody>
+    <tr><td>commit</td><td>Firefox 3.0.15</td><td>Safari 4.0.3</td></tr>
+    <tr><td>bb5899c56e33716db724cb60a5120b91f5fccdeb</td>
+    <td>28 ms/instantiation</td>
+    <td>15.02 ms/instantiation</td></tr>
+    <tr><td>2847c1cf1a2874e9fe56b5749e6e105e37bb086a</td>
+    <td>49.27 ms/instantiation</td>
+    <td>24.48 ms/instantiation</td></tr>
+    </tbody></table>
     </p>
   </body>
 </html>
index 4801a99..65f23a4 100644 (file)
       <input type="button" value="Y (2,4)" onclick="zoomGraphY(2,4)">&nbsp;
       <input type="button" value="Y (0,2)" onclick="zoomGraphY(0,2)">&nbsp;
       <input type="button" value="Y (0,1)" onclick="zoomGraphY(0,1)">&nbsp;
-      <br>
+      <br> <br>
       <input type="button" value="Oct 8-13" onclick="zoomGraphX(1160261979962, 1163905694248)">&nbsp;
       <input type="button" value="Oct 22-28" onclick="zoomGraphX(1161489164461 , 1162008465957)">&nbsp;
       <input type="button" value="Oct 23-24" onclick="zoomGraphX(1161575878860, 1161660991675)">&nbsp;
       <input type="button" value="Oct 26 6AM-noon" onclick="zoomGraphX(1161770537840, 1161792063332)">&nbsp;
-      <br>
+      <br> <br>
       <input type="button" value="Unzoom" onclick="unzoomGraph()">&nbsp;
+      <br> <br>
+      <input type="button" value="pan frame null" onclick="panEdgeFraction(null)">&nbsp;
+      <input type="button" value="pan frame 0.1" onclick="panEdgeFraction(0.1)">&nbsp;
+      <input type="button" value="pan frame 0.5" onclick="panEdgeFraction(0.5)">&nbsp;
       </p>
 
 
           valueRange: null
         });
       }
+
+      function panEdgeFraction(value) {
+        g.updateOptions({ panEdgeFraction : value });
+      }
     </script>
 
   </body>