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
<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>
}
</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);
}
}
}
* 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;
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;
}
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;
};
/**
* 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++) {
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
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;
--- /dev/null
+// 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
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",
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'));
}
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());
+}
<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>
}
};
-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,
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();
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) {
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),
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;
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;
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;
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;
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;
// 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];
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] = {
}
};
+/**
+ * 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 = [];
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;
// Add formatted labels to the ticks.
var k;
var k_labels = [];
+ var m_labels = [];
if (opts("labelsKMB")) {
k = 1000;
k_labels = [ "K", "M", "B", "T", "Q" ];
if (opts("labelsKMG2")) {
if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
k = 1024;
- k_labels = [ "k", "M", "G", "T", "P", "E" ];
+ k_labels = [ "k", "M", "G", "T", "P", "E", "Z", "Y" ];
+ m_labels = [ "m", "u", "n", "p", "f", "a", "z", "y" ];
}
var formatter = opts('axisLabelFormatter');
}
}
}
+ if(opts("labelsKMG2")){
+ tickV = String(tickV.toExponential());
+ if(tickV.split('e-').length === 2 && tickV.split('e-')[1] >= 3 && tickV.split('e-')[1] <= 24){
+ if(tickV.split('e-')[1] % 3 > 0) {
+ label = Dygraph.round_(tickV.split('e-')[0] /
+ Math.pow(10,(tickV.split('e-')[1] % 3)),
+ opts('digitsAfterDecimal'));
+ } else {
+ label = Number(tickV.split('e-')[0]).toFixed(2);
+ }
+ label += m_labels[Math.floor(tickV.split('e-')[1] / 3) - 1];
+ }
+ }
ticks[i].label = label;
}
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
<p>labelsKMG2 with yValueFormatter:</p>
<div id="labelsKMG2yValueFormatter" style="width:600px; height: 300px;"></div>
+ <p>labelsKMG2 with very small numbers:</p>
+ <div id="labelsKMG2SmallNumbers" style="width:600px; height: 300px;"></div>
+
<p>The curves are exponentials. Zooming in should reveal each of the 'K',
'M', 'B', etc. labels.</p>
return Math.round(num * shift)/shift;
};
- var data = [];
+ var data = [], smalldata = [];
for (var i = 0, n = 1; i < 63; i++, n *= 2) {
data.push([i, n]);
+ smalldata.push([i, Math.pow(10,-n)]);
}
var suffixes = ['', 'k', 'M', 'G', 'T'];
yValueFormatter: formatValue,
labels: ['Base', 'Power']
});
+
+ var g4 = new Dygraph(document.getElementById("labelsKMG2SmallNumbers"), smalldata, {
+ labelsKMG2: true,
+ labels: ['Base', 'Power']
+ });
</script>
</body>
</html>