From 7d1afbb93536f59e428f414d76514f25be4fffb6 Mon Sep 17 00:00:00 2001 From: Robert Konigsberg Date: Fri, 15 Jun 2012 13:26:37 -0400 Subject: [PATCH] Introduce Dygraph.createIterator, and use it to simplify the code in drawTrivialLine and drawNonTrivialLine. Lots o' tests. --- auto_tests/tests/utils_test.js | 81 ++++++++++++++++++++++++++++++++++++++++++ dygraph-canvas.js | 47 ++++++++++++------------ dygraph-utils.js | 57 +++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 22 deletions(-) diff --git a/auto_tests/tests/utils_test.js b/auto_tests/tests/utils_test.js index 44d543c..ef022b9 100644 --- a/auto_tests/tests/utils_test.js +++ b/auto_tests/tests/utils_test.js @@ -75,3 +75,84 @@ 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()); +} \ No newline at end of file diff --git a/dygraph-canvas.js b/dygraph-canvas.js index d588b9c..3e7a924 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -683,6 +683,7 @@ DygraphCanvasRenderer.prototype._renderAnnotations = function() { * But when it's true, the returned function will skip past points with null * yvals. * + * TODO(konigsberg): Replace the two uses of this function with the Dygraph iterator. * @private */ DygraphCanvasRenderer.makePointIteratorFunction_ = function( @@ -717,10 +718,16 @@ DygraphCanvasRenderer.prototype._drawStyledLine = function( var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName); ctx.save(); + + var iteratorPredicate = this.attr_("connectSeparatedPoints") ? + function(array, idx) { return array[idx].yval !== null; } : null; + var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + iteratorPredicate); + if (strokeWidth && !stepPlot && (!strokePattern || strokePattern.length <= 1)) { - this._drawTrivialLine(ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints); + this._drawTrivialLine(ctx, iter, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints); } else { - this._drawNonTrivialLine(ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, strokePattern, drawPointCallback, pointSize, drawPoints, drawGapPoints, stepPlot); + this._drawNonTrivialLine(ctx, iter, setName, color, strokeWidth, strokePattern, drawPointCallback, pointSize, drawPoints, drawGapPoints, stepPlot); } ctx.restore(); }; @@ -736,19 +743,16 @@ DygraphCanvasRenderer.prototype._drawPointsOnLine = function(ctx, pointsOnLine, } DygraphCanvasRenderer.prototype._drawNonTrivialLine = function( - ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, strokePattern, drawPointCallback, pointSize, drawPoints, drawGapPoints, stepPlot) { + ctx, iter, 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 nextFunc = DygraphCanvasRenderer.makePointIteratorFunction_( - this.attr_('connectSeparatedPoints'), points, firstIndexInSet, - firstIndexInSet + setLength); - for (var j = 0; j < setLength; j = nextFunc(j)) { - point = points[firstIndexInSet + j]; - nextY = (nextFunc(j) < setLength) ? - points[firstIndexInSet + nextFunc(j)].canvasy : null; + var first = true; + while(iter.hasNext()) { + point = iter.next(); + nextY = iter.hasNext() ? iter.peek().canvasy : null; if (DygraphCanvasRenderer.isNullOrNaN_(point.canvasy)) { if (stepPlot && prevX !== null) { // Draw a horizontal line to the start of the missing data @@ -767,8 +771,8 @@ DygraphCanvasRenderer.prototype._drawNonTrivialLine = function( 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) || - (nextFunc(j) < setLength && DygraphCanvasRenderer.isNullOrNaN_(nextY))) { + if ((!first && !prevX) || + (iter.hasNext() && DygraphCanvasRenderer.isNullOrNaN_(nextY))) { isIsolated = true; } } @@ -803,12 +807,13 @@ DygraphCanvasRenderer.prototype._drawNonTrivialLine = function( pointsOnLine.push([point.canvasx, point.canvasy]); } } + first = false; } this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize); }; DygraphCanvasRenderer.prototype._drawTrivialLine = function( - ctx, points, setLength, firstIndexInSet, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints) { + ctx, iter, setName, color, strokeWidth, drawPointCallback, pointSize, drawPoints, drawGapPoints) { var prevX = null; var prevY = null; var nextY = null; @@ -816,13 +821,10 @@ DygraphCanvasRenderer.prototype._drawTrivialLine = function( ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = strokeWidth; - var nextFunc = DygraphCanvasRenderer.makePointIteratorFunction_( - this.attr_('connectSeparatedPoints'), points, firstIndexInSet, - firstIndexInSet + setLength); - for (var j = firstIndexInSet; j < firstIndexInSet + setLength; j = nextFunc(j)) { - var nextJ = nextFunc(j); - var point = points[j]; - nextY = (nextJ < firstIndexInSet + setLength) ? points[nextJ].canvasy : null; + var first = true; + while(iter.hasNext()) { + var point = iter.next(); + nextY = iter.hasNext() ? iter.peek().canvasy : null; if (DygraphCanvasRenderer.isNullOrNaN_(point.canvasy)) { prevX = prevY = null; } else { @@ -830,8 +832,8 @@ DygraphCanvasRenderer.prototype._drawTrivialLine = function( if (drawGapPoints) { // Also consider a point to be is "isolated" if it's adjacent to a // null point, excluding the graph edges. - if ((j > firstIndexInSet && !prevX) || - ((nextJ < firstIndexInSet + setLength) && DygraphCanvasRenderer.isNullOrNaN_(nextY))) { + if ((!first && !prevX) || + (iter.hasNext() && DygraphCanvasRenderer.isNullOrNaN_(nextY))) { isIsolated = true; } } @@ -846,6 +848,7 @@ DygraphCanvasRenderer.prototype._drawTrivialLine = function( pointsOnLine.push([point.canvasx, point.canvasy]); } } + first = false; } ctx.stroke(); this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize); diff --git a/dygraph-utils.js b/dygraph-utils.js index 63411a5..c7b52d2 100644 --- a/dygraph-utils.js +++ b/dygraph-utils.js @@ -683,6 +683,63 @@ Dygraph.isAndroid = function() { /** * @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 + * @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. + * @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. + * + * TODO(konigsberg): add default vlues to start and length. + */ +Dygraph.createIterator = function(array, start, length, predicate) { + predicate = predicate || function() { return true; }; + + var iter = new function() { + this.idx_ = start - 1; // use -1 so initial call to advance works. + this.end_ = Math.min(array.length, start + length); + this.nextIdx_ = this.idx_; + var self = this; + + this.hasNext = function() { + return self.nextIdx_ < self.end_; + } + this.next = function() { + if (self.hasNext()) { + self.idx_ = self.nextIdx_; + self.advance_(); + return array[self.idx_]; + } + return null; + } + this.peek = function() { + if (self.hasNext()) { + return array[self.nextIdx_]; + } + return null; + } + this.advance_ = function() { + self.nextIdx_++; + while(self.hasNext()) { + if (predicate(array, self.nextIdx_)) { + return; + } + self.nextIdx_++; + } + } + }; + iter.advance_(); + return iter; +}; + +/** + * @private * Call a function N times at a given interval, then call a cleanup function * once. repeat_fn is called once immediately, then (times - 1) times * asynchronously. If times=1, then cleanup_fn() is also called synchronously. -- 2.7.4