Merge pull request #154 from dmoena/master
authorDan Vanderkam <danvdk@gmail.com>
Wed, 20 Jun 2012 15:23:14 +0000 (08:23 -0700)
committerDan Vanderkam <danvdk@gmail.com>
Wed, 20 Jun 2012 15:23:14 +0000 (08:23 -0700)
added support for labelsKMG2 to handle very small numbers (mili, micro, nano...)

README
auto_tests/misc/local.html
auto_tests/tests/CanvasAssertions.js
auto_tests/tests/missing_points.js [new file with mode: 0644]
auto_tests/tests/simple_drawing.js
auto_tests/tests/to_dom_coords.js
auto_tests/tests/utils_test.js
docs/data.html
dygraph-canvas.js
dygraph-layout.js
dygraph-utils.js

diff --git a/README b/README
index dd13dce..1dfd239 100644 (file)
--- a/README
+++ b/README
@@ -7,7 +7,7 @@ Source: http://github.com/danvk/dygraphs
 Issues: http://code.google.com/p/dygraphs/
 
 
-The dygraphs JavaScript library produces produces interactive, zoomable charts of time series.
+The dygraphs JavaScript library produces interactive, zoomable charts of time series.
 
 Features
 - Plots time series without using an external server or Flash
index e9feab4..961a9d5 100644 (file)
@@ -24,6 +24,7 @@
   <script type="text/javascript" src="../tests/error_bars.js"></script>
   <script type="text/javascript" src="../tests/formats.js"></script>
   <script type="text/javascript" src="../tests/interaction_model.js"></script>
+  <script type="text/javascript" src="../tests/missing_points.js"></script>
   <script type="text/javascript" src="../tests/multiple_axes.js"></script>
   <script type="text/javascript" src="../tests/multi_csv.js"></script>
   <script type="text/javascript" src="../tests/no_hours.js"></script>
@@ -59,8 +60,8 @@
   }
 </style>
   <script type="text/javascript">
-  var tc = null;
-  var name = null;
+  var tc = null; // Selected test case
+  var name = null; 
 
   var resultDiv = null;
 
       }
     }
     resultsDiv = createResultsDiv();
-    postResults(results);
+    var summary = { failed: 0, passed: 0 };
+    postResults(results, summary);
     resultsDiv.appendChild(document.createElement("hr"));
+    document.getElementById('summary').innerText = "(" + summary.failed + " failed, " + summary.passed + " passed)";
   }
 
   function createResultsDiv() {
     var body = document.getElementsByTagName("body")[0];
     div = document.createElement("div");
     div.id='results';
-    div.innerHTML = "Test results: <a href='#' id='passed'>passed</a> <a href='#' id='failed'>failed</a> <a href='#' id='all'>all</a><br/>";
+    div.innerHTML = "Test results: <span id='summary'></span> <a href='#' id='passed'>passed</a> <a href='#' id='failed'>failed</a> <a href='#' id='all'>all</a><br/>";
     body.insertBefore(div, body.firstChild);
 
     var setByClassName = function(name, displayStyle) {
     return div;
   }
 
-  function postResults(results, title) {
+  function postResults(results, summary, title) {
     if (typeof(results) == "boolean") {
       var elem = document.createElement("div");
       elem.setAttribute("class", results ? 'pass' : 'fail');
       var prefix = title ? (title + ": ") : "";
       elem.innerHTML = prefix + '<span class=\'outcome\'>' + (results ? 'pass' : 'fail') + '</span>';
       resultsDiv.appendChild(elem);
+      if (results) {
+        summary.passed++;
+      } else {
+        summary.failed++;
+      }
     } else { // hash
+      var failed = 0;
       var html = "";
       for (var key in results) {
         if (results.hasOwnProperty(key)) {
           var elem = results[key];
           if (typeof(elem) == "boolean" && title) {
-            postResults(results[key], title + "." + key);
+            postResults(results[key], summary, title + "." + key);
           } else {
-            postResults(results[key], key);
+            postResults(results[key], summary, key);
           }
         }
       }
index 6481c60..e8bb3e3 100644 (file)
@@ -35,10 +35,11 @@ var CanvasAssertions = {};
  * lineTo(p1) -> lineTo(p2)
  * lineTo(p2) -> lineTo(p1)
  *
- * attrs is meant to be used when you want to track things like
- * color and stroke width.
+ * predicate is meant to be used when you want to track things like
+ * color and stroke width. It can either be a hash of context properties,
+ * or a function that accepts the current call.
  */
-CanvasAssertions.assertLineDrawn = function(proxy, p1, p2, attrs) {
+CanvasAssertions.assertLineDrawn = function(proxy, p1, p2, predicate) {
   // found = 1 when prior loop found p1.
   // found = 2 when prior loop found p2.
   var priorFound = 0;
@@ -56,12 +57,14 @@ CanvasAssertions.assertLineDrawn = function(proxy, p1, p2, attrs) {
       var matchp2 = CanvasAssertions.matchPixels(p2, call.args);
       if (matchp1 || matchp2) {
         if (priorFound == 1 && matchp2) {
-// TODO -- add property test here  CanvasAssertions.matchAttributes(attrs, call.properties)
-          return;
+          if (CanvasAssertions.match(predicate, call)) {
+            return;
+          }
         }
         if (priorFound == 2 && matchp1) {
-       // TODO -- add property test here  CanvasAssertions.matchAttributes(attrs, call.properties)
-          return;
+          if (CanvasAssertions.match(predicate, call.properties)) {
+            return;
+          }
         }
         found = matchp1 ? 1 : 2;
       }
@@ -82,7 +85,38 @@ CanvasAssertions.assertLineDrawn = function(proxy, p1, p2, attrs) {
     return s + "}";
   };
   fail("Can't find a line drawn between " + p1 +
-      " and " + p2 + " with attributes " + toString(attrs));
+      " and " + p2 + " with attributes " + toString(predicate));
+};
+
+/**
+ * Return the lines drawn with specific attributes.
+ *
+ * This merely looks for one of these four possibilities:
+ * moveTo(p1) -> lineTo(p2)
+ * moveTo(p2) -> lineTo(p1)
+ * lineTo(p1) -> lineTo(p2)
+ * lineTo(p2) -> lineTo(p1)
+ *
+ * attrs is meant to be used when you want to track things like
+ * color and stroke width.
+ */
+CanvasAssertions.getLinesDrawn = function(proxy, predicate) {
+  var lastCall;
+  var lines = [];
+  for (var i = 0; i < proxy.calls__.length; i++) {
+    var call = proxy.calls__[i];
+
+    if (call.name == "lineTo") {
+      if (lastCall != null) {
+        if (CanvasAssertions.match(predicate, call)) {
+          lines.push([lastCall, call]);
+        }
+      }
+    }
+
+    lastCall = (call.name === "lineTo" || call.name === "moveTo") ? call : null;
+  }
+  return lines;
 };
 
 /**
@@ -111,6 +145,9 @@ CanvasAssertions.assertBalancedSaveRestore = function(proxy) {
  * Checks how many lines of the given color have been drawn.
  * @return {Integer} The number of lines of the given color.
  */
+// TODO(konigsberg): change 'color' to predicate? color is the
+// common case. Possibly allow predicate to be function, hash, or
+// string representing color?
 CanvasAssertions.numLinesDrawn = function(proxy, color) {
   var num_lines = 0;
   for (var i = 0; i < proxy.calls__.length; i++) {
@@ -122,6 +159,19 @@ CanvasAssertions.numLinesDrawn = function(proxy, color) {
   return num_lines;
 };
 
+/**
+ * Asserts that a series of lines are connected. For example,
+ * assertConsecutiveLinesDrawn(proxy, [[x1, y1], [x2, y2], [x3, y3]], predicate)
+ * is shorthand for
+ * assertLineDrawn(proxy, [x1, y1], [x2, y2], predicate)
+ * assertLineDrawn(proxy, [x2, y2], [x3, y3], predicate)
+ */
+CanvasAssertions.assertConsecutiveLinesDrawn = function(proxy, segments, predicate) {
+  for (var i = 0; i < segments.length - 1; i++) {
+    CanvasAssertions.assertLineDrawn(proxy, segments[i], segments[i+1], predicate);
+  }
+}
+
 CanvasAssertions.matchPixels = function(expected, actual) {
   // Expect array of two integers. Assuming the values are within one
   // integer unit of each other. This should be tightened down by someone
@@ -130,10 +180,23 @@ CanvasAssertions.matchPixels = function(expected, actual) {
       Math.abs(expected[1] - actual[1]) < 1;
 };
 
-CanvasAssertions.matchAttributes = function(expected, actual) {
-  for (var attr in expected) {
-    if (expected.hasOwnProperty(attr) && expected[attr] != actual[attr]) {
-      return false;
+/**
+ * For matching a proxy call against defined conditions.
+ * predicate can either by a hash of items compared against call.properties,
+ * or it can be a function that accepts the call, and returns true or false.
+ * If it's null, this function returns true.
+ */
+CanvasAssertions.match = function(predicate, call) {
+  if (predicate === null) {
+    return true;
+  }
+  if (typeof(predicate) === "function") {
+    return predicate(call);
+  } else {
+    for (var attr in predicate) {
+      if (predicate.hasOwnProperty(attr) && predicate[attr] != call.properties[attr]) {
+        return false;
+      }
     }
   }
   return true;
diff --git a/auto_tests/tests/missing_points.js b/auto_tests/tests/missing_points.js
new file mode 100644 (file)
index 0000000..594282c
--- /dev/null
@@ -0,0 +1,169 @@
+// Copyright (c) 2012 Google, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/** 
+ * @fileoverview Test cases for drawing lines with missing points.
+ *
+ * @author konigsberg@google.com (Robert Konigsberg)
+ */
+var ZERO_TO_FIFTY = [[ 10, 0 ] , [ 20, 50 ]];
+
+var MissingPointsTestCase = TestCase("missing-points");
+
+var _origFunc = Dygraph.getContext;
+MissingPointsTestCase.prototype.setUp = function() {
+  document.body.innerHTML = "<div id='graph'></div>";
+  Dygraph.getContext = function(canvas) {
+    return new Proxy(_origFunc(canvas));
+  }
+};
+
+MissingPointsTestCase.prototype.tearDown = function() {
+  Dygraph.getContext = _origFunc;
+};
+
+MissingPointsTestCase.prototype.testSeparatedPointsDontDraw = function() {
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(
+      graph,
+      [[1, 10, 11],
+       [2, 11, null],
+       [3, 12, 13]],
+      { colors: ['red', 'blue']});
+  var htx = g.hidden_ctx_;
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assertEquals(0, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+}
+
+MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded = function() {
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(
+      graph,
+      [[0, 10],
+       [1, 11],
+       [2, null],
+       [3, 13],
+       [4, 14]],
+      { colors: ['blue']});
+  var htx = g.hidden_ctx_;
+
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  CanvasAssertions.assertLineDrawn(htx, [56, 275], [161, 212],
+      { strokeStyle: '#0000ff', });
+  CanvasAssertions.assertLineDrawn(htx, [370, 87], [475, 25],
+      { strokeStyle: '#0000ff', });
+}
+
+MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded_connected = function() {
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(
+      graph,
+      [[0, 10],
+       [1, 11],
+       [2, null],
+       [3, 13],
+       [4, 14]],
+      { colors: ['blue'], connectSeparatedPoints: true});
+  var htx = g.hidden_ctx_;
+  var num_lines = 0;
+
+  assertEquals(3, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  CanvasAssertions.assertConsecutiveLinesDrawn(htx, 
+      [[56, 275], [161, 212], [370, 87], [475, 25]],
+      { strokeStyle: '#0000ff' });
+}
+
+/**
+ * At the time of writing this test, the blue series is only points, and not lines.
+ */
+MissingPointsTestCase.prototype.testConnectSeparatedPoints = function() {
+  var g = new Dygraph(
+    document.getElementById("graph"),
+    [
+      [1, null, 3],
+      [2, 2, null],
+      [3, null, 7],
+      [4, 5, null],
+      [5, null, 5],
+      [6, 3, null]
+    ],
+    {
+      connectSeparatedPoints: true,
+      drawPoints: true,
+      colors: ['red', 'blue']
+    }
+  );
+
+  var htx = g.hidden_ctx_;
+
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  CanvasAssertions.assertConsecutiveLinesDrawn(htx, 
+      [[56, 225], [223, 25], [391, 125]],
+      { strokeStyle: '#0000ff' });
+
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  CanvasAssertions.assertConsecutiveLinesDrawn(htx, 
+      [[140, 275], [307, 125], [475, 225]],
+      { strokeStyle: '#ff0000' });
+}
+
+/**
+ * At the time of writing this test, the blue series is only points, and not lines.
+ */
+MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() {
+  var g = new Dygraph(
+    document.getElementById("graph"),
+    "x,A,B  \n" +
+    "1,,3   \n" +
+    "2,2,   \n" +
+    "3,,5   \n" +
+    "4,4,   \n" +
+    "5,,7   \n" +
+    "6,NaN, \n" +
+    "8,8,   \n" +
+    "10,10, \n",
+    {
+      connectSeparatedPoints: true,
+      drawPoints: true,
+      colors: ['red', 'blue']
+    }
+  );
+
+  var htx = g.hidden_ctx_;
+
+  // Red has two disconnected line segments
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  CanvasAssertions.assertLineDrawn(htx, [102, 275], [195, 212], { strokeStyle: '#ff0000' });
+  CanvasAssertions.assertLineDrawn(htx, [381, 87], [475, 25], { strokeStyle: '#ff0000' });
+
+  // Blue's lines are consecutive, however.
+  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  CanvasAssertions.assertConsecutiveLinesDrawn(htx, 
+      [[56, 244], [149, 181], [242, 118]],
+      { strokeStyle: '#0000ff' });
+}
+
+/* These lines contain awesome powa!  
+  var lines = CanvasAssertions.getLinesDrawn(htx, {strokeStyle: "#0000ff"});
+  for (var idx = 0; idx < lines.length; idx++) {
+    var line = lines[idx];
+    console.log(line[0].args, line[1].args, line[0].properties.strokeStyle);
+  }
+*/
\ No newline at end of file
index 8f3b0c5..b2ddccf 100644 (file)
@@ -49,7 +49,7 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, ZERO_TO_FIFTY, opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   CanvasAssertions.assertLineDrawn(htx, [0,320], [475,6.2745], {
     strokeStyle: "#008080",
index c70b704..98da7c4 100644 (file)
@@ -56,6 +56,7 @@ ToDomCoordsTestCase.prototype.testPlainChart = function() {
 
   this.checkForInverses(g);
 
+  // TODO(konigsberg): This doesn't really belong here. Move to its own test.
   var htx = g.hidden_ctx_;
   assertEquals(1, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
 }
index 44d543c..9c8974c 100644 (file)
@@ -75,3 +75,98 @@ UtilsTestCase.prototype.testUpdateDeepDecoupled = function() {
   b.c.x = "new value";
   assertEquals("original", a.c.x);
 };
+
+
+UtilsTestCase.prototype.testIterator_nopredicate = function() {
+  var array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
+  var iter = Dygraph.createIterator(array, 1, 4);
+  assertTrue(iter.hasNext());
+  assertEquals('b', iter.peek());
+  assertEquals('b', iter.next());
+  assertTrue(iter.hasNext());
+
+  assertEquals('c', iter.peek());
+  assertEquals('c', iter.next());
+
+  assertTrue(iter.hasNext());
+  assertEquals('d', iter.next());
+
+  assertTrue(iter.hasNext());
+  assertEquals('e', iter.next());
+
+  assertFalse(iter.hasNext());
+}
+
+UtilsTestCase.prototype.testIterator_predicate = function() {
+  var array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
+  var iter = Dygraph.createIterator(array, 1, 4,
+      function(array, idx) { return array[idx] !== 'd' });
+  assertTrue(iter.hasNext());
+  assertEquals('b', iter.peek());
+  assertEquals('b', iter.next());
+  assertTrue(iter.hasNext());
+
+  assertEquals('c', iter.peek());
+  assertEquals('c', iter.next());
+
+  assertTrue(iter.hasNext());
+  assertEquals('e', iter.next());
+
+  assertFalse(iter.hasNext());
+}
+
+UtilsTestCase.prototype.testIterator_empty = function() {
+  var array = [];
+  var iter = Dygraph.createIterator([], 0, 0);
+  assertFalse(iter.hasNext());
+}
+
+UtilsTestCase.prototype.testIterator_outOfRange = function() {
+  var array = ['a', 'b', 'c'];
+  var iter = Dygraph.createIterator(array, 1, 4,
+      function(array, idx) { return array[idx] !== 'd' });
+  assertTrue(iter.hasNext());
+  assertEquals('b', iter.peek());
+  assertEquals('b', iter.next());
+  assertTrue(iter.hasNext());
+
+  assertEquals('c', iter.peek());
+  assertEquals('c', iter.next());
+
+  assertFalse(iter.hasNext());
+}
+
+// Makes sure full array is tested, and that the predicate isn't called
+// with invalid boundaries.
+UtilsTestCase.prototype.testIterator_whole_array = function() {
+  var array = ['a', 'b', 'c'];
+  var iter = Dygraph.createIterator(array, 0, array.length,
+      function(array, idx) {
+        if (idx < 0 || idx >= array.length) {
+          throw "err";
+        } else {
+          return true;
+        };
+      });
+  assertTrue(iter.hasNext());
+  assertEquals('a', iter.next());
+  assertTrue(iter.hasNext());
+  assertEquals('b', iter.next());
+  assertTrue(iter.hasNext());
+  assertEquals('c', iter.next());
+  assertFalse(iter.hasNext());
+  assertNull(iter.next());
+}
+
+UtilsTestCase.prototype.testIterator_no_args = function() {
+  var array = ['a', 'b', 'c'];
+  var iter = Dygraph.createIterator(array);
+  assertTrue(iter.hasNext());
+  assertEquals('a', iter.next());
+  assertTrue(iter.hasNext());
+  assertEquals('b', iter.next());
+  assertTrue(iter.hasNext());
+  assertEquals('c', iter.next());
+  assertFalse(iter.hasNext());
+  assertNull(iter.next());
+}
index b010407..135fe16 100644 (file)
       <i>customBars</i>: [x, [low1, val1, high1], [low2, val2, high2], ...]
     </code>
 
-    <p>To specify missing data, set the value to null. You may not set a value
-    inside an array to null. Use null instead of the entire array.</p>
+    <p>To specify missing data, set the value to null or NaN. You may not set a value
+    inside an array to null or NaN. Use null or NaN instead of the entire array. 
+    The only difference between the two is when the option
+    <a href="options.html#conectSeparatedPoints">connectSeparatedPoints</a>
+    true. In that case, the gaps created by nulls are filled in, and gaps
+    created by NaNs are preserved.
+    </p>
 
     <a name="function"><h3>Functions</h3>
 
index de8d4fe..4a29848 100644 (file)
@@ -677,23 +677,18 @@ DygraphCanvasRenderer.prototype._renderAnnotations = function() {
   }
 };
 
-DygraphCanvasRenderer.makeNextPointStep_ = function(
-    connect, points, start, end) {
-  if (connect) {
-    return function(j) {
-      while (++j + start < end) {
-        if (!(points[start + j].yval === null)) break;
-      }
-      return j;
-    }
-  } else {
-    return function(j) { return j + 1 };
-  }
-};
+/**
+ * Returns a predicate to be used with an iterator, which will
+ * iterate over points appropriately, depending on whether
+ * connectSeparatedPoints is true. When it's false, the predicate will
+ * skip over points with missing yVals.
+ */
+DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
+  return connectSeparatedPoints ? DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : null;
+}
 
-DygraphCanvasRenderer.isNullOrNaN_ = function(x) {
-  return (x === null || isNaN(x));
-};
+DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
+  function(array, idx) { return array[idx].yval !== null; }
 
 DygraphCanvasRenderer.prototype._drawStyledLine = function(
     ctx, i, setName, color, strokeWidth, strokePattern, drawPoints,
@@ -709,83 +704,70 @@ DygraphCanvasRenderer.prototype._drawStyledLine = function(
   var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName);
 
   ctx.save();
-  if (strokeWidth && !stepPlot && (!strokePattern || strokePattern.length <= 1)) {
-    this._drawTrivialLine(ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints);
+
+  var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
+      DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
+
+  var pointsOnLine;
+  var strategy;
+  if (!strokePattern || strokePattern.length <= 1) {
+    strategy = trivialStrategy(ctx, color, strokeWidth);
   } else {
-    this._drawNonTrivialLine(ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, strokePattern, drawPointCallback, pointSize, drawPoints, drawGapPoints, stepPlot);
+    strategy = nonTrivialStrategy(this, ctx, color, strokeWidth, strokePattern);
   }
+  pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, strategy);
+  this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize);
+
   ctx.restore();
 };
 
-DygraphCanvasRenderer.prototype._drawNonTrivialLine = function(
-    ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, strokePattern, drawPointCallback, pointSize, drawPoints, drawGapPoints, stepPlot) {
-  var prevX = null;
-  var prevY = null;
-  var nextY = null;
-  var point, nextPoint;
-  var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
-  var next = DygraphCanvasRenderer.makeNextPointStep_(
-      this.attr_('connectSeparatedPoints'), points, firstIndexInSet,
-      firstIndexInSet + setLength);
-  for (var j = 0; j < setLength; j = next(j)) {
-    point = points[firstIndexInSet + j];
-    nextY = (next(j) < setLength) ?
-        points[firstIndexInSet + next(j)].canvasy : null;
-    if (DygraphCanvasRenderer.isNullOrNaN_(point.canvasy)) {
-      if (stepPlot && prevX !== null) {
-        // Draw a horizontal line to the start of the missing data
-        ctx.beginPath();
-        ctx.strokeStyle = color;
-        ctx.lineWidth = this.attr_('strokeWidth');
-        this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
-        ctx.stroke();
-      }
-      // this will make us move to the next point, not draw a line to it.
-      prevX = prevY = null;
-    } else {
-      // A point is "isolated" if it is non-null but both the previous
-      // and next points are null.
-      var isIsolated = (!prevX && DygraphCanvasRenderer.isNullOrNaN_(nextY));
-      if (drawGapPoints) {
-        // Also consider a point to be is "isolated" if it's adjacent to a
-        // null point, excluding the graph edges.
-        if ((j > 0 && !prevX) ||
-            (next(j) < setLength && DygraphCanvasRenderer.isNullOrNaN_(nextY))) {
-          isIsolated = true;
-        }
-      }
-      if (prevX === null) {
-        prevX = point.canvasx;
-        prevY = point.canvasy;
-      } else {
-        // Skip over points that will be drawn in the same pixel.
-        if (Math.round(prevX) == Math.round(point.canvasx) &&
-            Math.round(prevY) == Math.round(point.canvasy)) {
-          continue;
-        }
-        // TODO(antrob): skip over points that lie on a line that is already
-        // going to be drawn. There is no need to have more than 2
-        // consecutive points that are collinear.
-        if (strokeWidth) {
-          ctx.beginPath();
-          ctx.strokeStyle = color;
-          ctx.lineWidth = strokeWidth;
-          if (stepPlot) {
-            this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
-            prevX = point.canvasx;
-          }
-          this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
-          prevX = point.canvasx;
-          prevY = point.canvasy;
-          ctx.stroke();
-        }
-      }
+var nonTrivialStrategy = function(renderer, ctx, color, strokeWidth, strokePattern) {
+  return new function() {
+    this.init = function() {  };
+    this.finish = function() { };
+    this.startSegment = function() {
+       ctx.beginPath();
+       ctx.strokeStyle = color;
+       ctx.lineWidth = strokeWidth;
+    };
+    this.endSegment = function() {
+      ctx.stroke(); // should this include closePath?
+    };
+    this.drawLine = function(x1, y1, x2, y2) {
+      renderer._dashedLine(ctx, x1, y1, x2, y2, strokePattern);
+    };
+    this.skipPixel = function(prevX, prevY, curX, curY) {
+      // TODO(konigsberg): optimize with http://jsperf.com/math-round-vs-hack/6 ?
+      return (Math.round(prevX) == Math.round(curX) &&
+           Math.round(prevY) == Math.round(curY));
+    };
+  };
+};
 
-      if (drawPoints || isIsolated) {
-        pointsOnLine.push([point.canvasx, point.canvasy]);
-      }
-    }
-  }
+var trivialStrategy = function(ctx, color, strokeWidth) {
+  return new function() {
+    this.init = function() {
+      ctx.beginPath();
+      ctx.strokeStyle = color;
+      ctx.lineWidth = strokeWidth;
+    };
+    this.finish = function() {
+      ctx.stroke(); // should this include closePath?
+    };
+    this.startSegment = function() { };
+    this.endSegment = function() { };
+    this.drawLine = function(x1, y1, x2, y2) {
+      ctx.moveTo(x1, y1);
+      ctx.lineTo(x2, y2);
+    };
+    // don't skip pixels.
+    this.skipPixel = function() {
+      return false;
+    };
+  };
+};
+
+DygraphCanvasRenderer.prototype._drawPointsOnLine = function(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize) {
   for (var idx = 0; idx < pointsOnLine.length; idx++) {
     var cb = pointsOnLine[idx];
     ctx.save();
@@ -793,54 +775,70 @@ DygraphCanvasRenderer.prototype._drawNonTrivialLine = function(
         this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize);
     ctx.restore();
   }
-};
+}
+
+DygraphCanvasRenderer.prototype._drawSeries = function(
+    ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints,
+    stepPlot, strategy) {
 
-DygraphCanvasRenderer.prototype._drawTrivialLine = function(
-    ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints) {
-  var prevX = null;
-  var prevY = null;
-  var nextY = null;
+  var prevCanvasX = null;
+  var prevCanvasY = null;
+  var nextCanvasY = null;
+  var isIsolated; // true if this point is isolated (no line segments)
+  var point; // the point being processed in the while loop
   var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
-  ctx.beginPath();
-  ctx.strokeStyle = color;
-  ctx.lineWidth = strokeWidth;
-  for (var j = firstIndexInSet; j < firstIndexInSet + setLength; ++j) {
-    var point = points[j];
-    nextY = (j + 1 < firstIndexInSet + setLength) ? points[j + 1].canvasy : null;
-    if (DygraphCanvasRenderer.isNullOrNaN_(point.canvasy)) {
-      prevX = prevY = null;
+  var first = true; // the first cycle through the while loop
+
+  strategy.init();
+
+  while(iter.hasNext()) {
+    point = iter.next();
+    if (point.canvasy === null || point.canvasy != point.canvasy) {
+      if (stepPlot && prevCanvasX !== null) {
+        // Draw a horizontal line to the start of the missing data
+        strategy.startSegment();
+        strategy.drawLine(prevX, prevY, point.canvasx, prevY);
+        strategy.endSegment();
+      }
+      prevCanvasX = prevCanvasY = null;
     } else {
-      var isIsolated = (!prevX && DygraphCanvasRenderer.isNullOrNaN_(nextY));
+      nextCanvasY = iter.hasNext() ? iter.peek().canvasy : null;
+      // TODO: we calculate isNullOrNaN for this point, and the next, and then, when
+      // we iterate, test for isNullOrNaN again. Why bother?
+      var isNextCanvasYNullOrNaN = nextCanvasY === null || nextCanvasY != nextCanvasY;
+      isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN);
       if (drawGapPoints) {
-        // Also consider a point to be is "isolated" if it's adjacent to a
+        // Also consider a point to be "isolated" if it's adjacent to a
         // null point, excluding the graph edges.
-        if ((j > firstIndexInSet && !prevX) ||
-            ((j + 1 < firstIndexInSet + setLength) && DygraphCanvasRenderer.isNullOrNaN_(nextY))) {
+        if ((!first && !prevCanvasX) ||
+            (iter.hasNext() && isNextCanvasYNullOrNaN)) {
           isIsolated = true;
         }
       }
-      if (prevX === null) {
-        prevX = point.canvasx;
-        prevY = point.canvasy;
-        if (j === firstIndexInSet) {
-          ctx.moveTo(point.canvasx, point.canvasy);
+      if (prevCanvasX !== null) {
+        if (strategy.skipPixel(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy)) {
+          continue;
+        }
+        if (strokeWidth) {
+          strategy.startSegment();
+          if (stepPlot) {
+            strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, prevCanvasY);
+            prevCanvasX = point.canvasx;
+          }
+          strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy);      
+          strategy.endSegment();
         }
-      } else {
-        ctx.lineTo(point.canvasx, point.canvasy);
       }
       if (drawPoints || isIsolated) {
         pointsOnLine.push([point.canvasx, point.canvasy]);
       }
+      prevCanvasX = point.canvasx;
+      prevCanvasY = point.canvasy;
     }
+    first = false;
   }
-  ctx.stroke();
-  for (var idx = 0; idx < pointsOnLine.length; idx++) {
-    var cb = pointsOnLine[idx];
-    ctx.save();
-    drawPointCallback(
-        this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize);
-    ctx.restore();
-  }
+  strategy.finish();
+  return pointsOnLine;
 };
 
 DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) {
@@ -852,7 +850,6 @@ DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) {
   var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) ||
       Dygraph.Circles.DEFAULT;
 
-  // TODO(konigsberg): Turn this into one call, and then consider inlining drawStyledLine.
   if (borderWidth && strokeWidth) {
     this._drawStyledLine(ctx, i, setName,
         this.dygraph_.attr_("strokeBorderColor", setName),
@@ -887,7 +884,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
   var stepPlot = this.attr_("stepPlot");
   var points = this.layout.points;
   var pointsLength = points.length;
-  var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
+  var point, i, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
 
   var setNames = this.layout.setNames;
   var setCount = setNames.length;
@@ -924,11 +921,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
 
       var firstIndexInSet = this.layout.setPointsOffsets[i];
       var setLength = this.layout.setPointsLengths[i];
-      var afterLastIndexInSet = firstIndexInSet + setLength;
 
-      var next = DygraphCanvasRenderer.makeNextPointStep_(
-        this.attr_('connectSeparatedPoints'), points,
-        afterLastIndexInSet);
+      var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
+          DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
 
       // setup graphics context
       prevX = NaN;
@@ -941,8 +936,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
                             fillAlpha + ')';
       ctx.fillStyle = err_color;
       ctx.beginPath();
-      for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) {
-        point = points[j];
+      while (iter.hasNext()) {
+        point = iter.next();
         if (point.name == setName) { // TODO(klausw): this is always true
           if (!Dygraph.isOK(point.y)) {
             prevX = NaN;
@@ -996,11 +991,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
       axisY = this.area.h * axisY + this.area.y;
       var firstIndexInSet = this.layout.setPointsOffsets[i];
       var setLength = this.layout.setPointsLengths[i];
-      var afterLastIndexInSet = firstIndexInSet + setLength;
 
-      var next = DygraphCanvasRenderer.makeNextPointStep_(
-        this.attr_('connectSeparatedPoints'), points,
-        afterLastIndexInSet);
+      var iter = Dygraph.createIterator(points, firstIndexInSet, setLength,
+          DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
 
       // setup graphics context
       prevX = NaN;
@@ -1012,8 +1005,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
                             fillAlpha + ')';
       ctx.fillStyle = err_color;
       ctx.beginPath();
-      for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) {
-        point = points[j];
+      while(iter.hasNext()) {
+        point = iter.next();
         if (point.name == setName) { // TODO(klausw): this is always true
           if (!Dygraph.isOK(point.y)) {
             prevX = NaN;
index 1fe4228..ac1eca0 100644 (file)
@@ -219,16 +219,17 @@ DygraphLayout.prototype._evaluateLineCharts = function() {
   // points and drawing the lines. The brunt of the cost comes from allocating
   // the |point| structures.
   var i = 0;
+  var setIdx;
 
   // Preallocating the size of points reduces reallocations, and therefore,
   // calls to collect garbage.
   var totalPoints = 0;
-  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+  for (setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
     totalPoints += this.datasets[setIdx].length;
   }
   this.points = new Array(totalPoints);
 
-  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+  for (setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
     this.setPointsOffsets.push(i);
     var dataset = this.datasets[setIdx];
     var setName = this.setNames[setIdx];
@@ -236,15 +237,15 @@ DygraphLayout.prototype._evaluateLineCharts = function() {
 
     for (var j = 0; j < dataset.length; j++) {
       var item = dataset[j];
-      var xValue = item[0];
-      var yValue = item[1];
+      var xValue = DygraphLayout.parseFloat_(item[0]);
+      var yValue = DygraphLayout.parseFloat_(item[1]);
 
       // Range from 0-1 where 0 represents left and 1 represents right.
       var xNormal = (xValue - this.minxval) * this.xscale;
       // Range from 0-1 where 0 represents top and 1 represents bottom
       var yNormal = DygraphLayout._calcYNormal(axis, yValue);
 
-      if (connectSeparated && yValue === null) {
+      if (connectSeparated && item[1] === null) {
         yValue = null;
       }
       this.points[i] = {
@@ -261,6 +262,20 @@ DygraphLayout.prototype._evaluateLineCharts = function() {
   }
 };
 
+/**
+ * Optimized replacement for parseFloat, which was way too slow when almost
+ * all values were type number, with few edge cases, none of which were strings.
+ */
+DygraphLayout.parseFloat_ = function(val) {
+  // parseFloat(null) is NaN
+  if (val === null) {
+    return NaN;
+  }
+
+  // Assume it's a number or NaN. If it's something else, I'll be shocked.
+  return val;
+}
+
 DygraphLayout.prototype._evaluateLineTicks = function() {
   var i, tick, label, pos;
   this.xticks = [];
@@ -305,13 +320,13 @@ DygraphLayout.prototype.evaluateWithError = function() {
     var axis = this.dygraph_.axisPropertiesForSeries(setName);
     for (j = 0; j < dataset.length; j++, i++) {
       var item = dataset[j];
-      var xv = item[0];
-      var yv = item[1];
+      var xv = DygraphLayout.parseFloat_(item[0]);
+      var yv = DygraphLayout.parseFloat_(item[1]);
 
       if (xv == this.points[i].xval &&
           yv == this.points[i].yval) {
-        var errorMinus = item[2];
-        var errorPlus = item[3];
+        var errorMinus = DygraphLayout.parseFloat_(item[2]);
+        var errorPlus = DygraphLayout.parseFloat_(item[3]);
 
         var yv_minus = yv - errorMinus;
         var yv_plus = yv + errorPlus;
index 63411a5..8cfd861 100644 (file)
@@ -681,6 +681,70 @@ Dygraph.isAndroid = function() {
   return (/Android/).test(navigator.userAgent);
 };
 
+Dygraph.Iterator = function(array, start, length, predicate) {
+  start = start || 0;
+  length = length || array.length;
+  this.array_ = array;
+  this.predicate_ = predicate;
+  this.end_ = Math.min(array.length, start + length);
+  this.nextIdx_ = start - 1; // use -1 so initial call to advance works.
+  this.hasNext_ = true;
+  this.peek_ = null;
+  this.advance_();
+}
+
+Dygraph.Iterator.prototype.hasNext = function() {
+  return this.hasNext_;
+}
+
+Dygraph.Iterator.prototype.next = function() {
+  if (this.hasNext_) {
+    var obj = this.peek_;
+    this.advance_();
+    return obj;
+  }
+  return null;
+}
+
+Dygraph.Iterator.prototype.peek = function() {
+  return this.peek_;
+}
+
+Dygraph.Iterator.prototype.advance_ = function() {
+  var nextIdx = this.nextIdx_;
+  nextIdx++;
+  while(nextIdx < this.end_) {
+    if (!this.predicate_ || this.predicate_(this.array_, nextIdx)) {
+      this.peek_ = this.array_[nextIdx];
+      this.nextIdx_ = nextIdx;
+      return;
+    }
+    nextIdx++;
+  }
+  this.nextIdx_ = nextIdx;
+  this.hasNext_ = false;
+  this.peek_ = null;
+}
+
+/**
+ * @private
+ * Returns a new iterator over array, between indexes start and 
+ * start + length, and only returns entries that pass the accept function
+ *
+ * @param array the array to iterate over.
+ * @param start the first index to iterate over, 0 if absent.
+ * @param length the number of elements in the array to iterate over.
+ * This, along with start, defines a slice of the array, and so length
+ * doesn't imply the number of elements in the iterator when accept
+ * doesn't always accept all values. array.length when absent.
+ * @param predicate a function that takes parameters array and idx, which
+ * returns true when the element should be returned. If omitted, all
+ * elements are accepted.
+ */
+Dygraph.createIterator = function(array, start, length, predicate) {
+  return new Dygraph.Iterator(array, start, length, predicate);
+};
+
 /**
  * @private
  * Call a function N times at a given interval, then call a cleanup function