From: Robert Konigsberg Date: Mon, 3 Jun 2013 14:15:14 +0000 (-0400) Subject: Merge branch 'i382' of https://github.com/kberg/dygraphs into i382 X-Git-Tag: v1.0.0~27^2~3^2 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=400a62b3df5b199814df17d57423f8c789d132de;hp=9b18154be1bbb22ba99a17993ea58ffdeccc7ff9;p=dygraphs.git Merge branch 'i382' of https://github.com/kberg/dygraphs into i382 --- diff --git a/auto_tests/misc/fake-jstestdriver.js b/auto_tests/misc/fake-jstestdriver.js index 6f4f464..404d165 100644 --- a/auto_tests/misc/fake-jstestdriver.js +++ b/auto_tests/misc/fake-jstestdriver.js @@ -24,7 +24,19 @@ * @author konigsberg@google.com (Robert Konigsberg) */ var jstestdriver = { - jQuery : jQuery + jQuery : jQuery, + listeners_ : [], + announce_ : function(name, args) { + for (var idx = 0; idx < jstestdriver.listeners_.length; idx++) { + var listener = jstestdriver.listeners_[idx]; + if (listener[name]) { + listener[name].apply(null, args); + } + } + }, + attachListener: function(listener) { + jstestdriver.listeners_.push(listener); + } }; if (!console) { @@ -55,6 +67,7 @@ function TestCase(name) { testCase.prototype.setUp = function() { }; testCase.prototype.tearDown = function() { }; + testCase.prototype.name = name; /** * name can be a string, which is looked up in this object, or it can be a * function, in which case it's run. @@ -68,29 +81,34 @@ function TestCase(name) { * Chrome's console completion. */ testCase.prototype.runTest = function(func) { + var result = false; + var ex = null; + var name = typeof(func) == "string" ? func : "(anonymous function)"; + jstestdriver.announce_("start", [this, name]); try { - this.setUp(); - - var fn = null; - var parameterType = typeof(func); - if (typeof(func) == "function") { - fn = func; - } else if (typeof(func) == "string") { - fn = this[func]; - } else { - fail("can't supply " + typeof(func) + " to runTest"); - } - - fn.apply(this, []); - this.tearDown(); - return true; + result = this.runTest_(func); } catch (e) { - console.log(e); - if (e.stack) { - console.log(e.stack); - } - return false; + ex = e; } + jstestdriver.announce_("finish", [this, name, result, ex]); + } + + testCase.prototype.runTest_ = function(func) { + this.setUp(); + + var fn = null; + var parameterType = typeof(func); + if (typeof(func) == "function") { + fn = func; + } else if (typeof(func) == "string") { + fn = this[func]; + } else { + fail("can't supply " + typeof(func) + " to runTest"); + } + + fn.apply(this, []); + this.tearDown(); + return true; }; testCase.prototype.runAllTests = function() { @@ -152,3 +170,15 @@ function addGlobalTestSymbols() { function getAllTestCases() { return testCaseList; } + +jstestdriver.attachListener({ + finish : function(tc, name, result, e) { + if (e) { + console.log(e); + if (e.stack) { + console.log(e.stack); + } + } + } +}); + diff --git a/auto_tests/misc/local.html b/auto_tests/misc/local.html index 74f57e6..6950a0a 100644 --- a/auto_tests/misc/local.html +++ b/auto_tests/misc/local.html @@ -17,23 +17,26 @@ + + + + - @@ -71,161 +74,11 @@ color: black; text-decoration: none; } - - +
@@ -244,45 +97,11 @@ Example: local.html?testCase=ScrollingDivTestCase&test=testNestedDiv_Scrolled&command=runTest

- diff --git a/auto_tests/misc/local.js b/auto_tests/misc/local.js new file mode 100644 index 0000000..39a3cb5 --- /dev/null +++ b/auto_tests/misc/local.js @@ -0,0 +1,248 @@ +var DygraphsLocalTester = function() { + this.tc = null; // Selected test case + this.name = null; + this.resultsDiv = null; + this.results = []; + this.summary = { failed: 0, passed: 0 }; + + var self = this; + jstestdriver.attachListener({ + start : function(tc) { + self.start_(tc); + }, + finish : function(tc, name, result, e) { + self.finish_(tc, name, result, e); + } + }); +}; + +/** + * Call this to replace Dygraphs.warn so it throws an error. + * + * In some cases we will still allow warnings to be warnings, however. + */ +DygraphsLocalTester.prototype.overrideWarn = function() { + // save Dygraph.warn so we can catch warnings. + var originalDygraphWarn = Dygraph.warn; + Dygraph.warn = function(msg) { + // This warning is still + if (msg == "Using default labels. Set labels explicitly via 'labels' in the options parameter") { + originalDygraphWarn(msg); + return; + } + throw "Warnings not permitted: " + msg; + } + Dygraph.prototype.warn = Dygraph.warn; +}; + +DygraphsLocalTester.prototype.processVariables = function() { + var splitVariables = function() { // http://www.idealog.us/2006/06/javascript_to_p.html + var query = window.location.search.substring(1); + var args = {}; + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + if (vars[i].length > 0) { + var pair = vars[i].split("="); + args[pair[0]] = pair[1]; + } + } + return args; + } + + var args = splitVariables(); + var test = args.test; + var command = args.command; + + // args.testCaseName uses the string name of the test. + if (args.testCaseName) { + var testCases = getAllTestCases(); + name = args.testCaseName; + for (var idx in testCases) { + var entry = testCases[idx]; + if (entry.name == args.testCaseName) { + var prototype = entry.testCase; + this.tc = new entry.testCase(); + break; + } + } + } else if (args.testCase) { // The class name of the test. + name = args.testCase; + eval("tc__= new " + args.testCase + "()"); + this.tc = tc_; + } + + // If the test class is defined. + if (this.tc != null) { + if (args.command == "runAllTests") { + console.log("Running all tests for " + args.testCase); + this.tc.runAllTests(); + } else if (args.command == "runTest") { + console.log("Running test " + args.testCase + "." + args.test); + this.tc.runTest(args.test); + } + } else { + if (args.command == "runAllTests") { + console.log("Running all tests for all test cases"); + var testCases = getAllTestCases(); + for (var idx in testCases) { + var entry = testCases[idx]; + var prototype = entry.testCase; + this.tc = new entry.testCase(); + this.tc.runAllTests(); + } + } + } + this.resultsDiv = this.createResultsDiv(); + this.postResults(); + this.resultsDiv.appendChild(document.createElement("hr")); + document.getElementById('summary').innerHTML = "(" + this.summary.failed + " failed, " + this.summary.passed + " passed)"; +} + +DygraphsLocalTester.prototype.createResultsDiv = function() { + div = document.createElement("div"); + div.id='results'; + div.innerHTML = "Test results: passed failed all
"; + + var body = document.getElementsByTagName("body")[0]; + body.insertBefore(div, body.firstChild); + + var setByClassName = function(name, displayStyle) { + var elements = div.getElementsByClassName(name); + for (var i = 0; i < elements.length; i++) { + elements[i].style.display = displayStyle; + } + } + + var passedAnchor = document.getElementById('passed'); + var failedAnchor = document.getElementById('failed'); + var allAnchor = document.getElementById('all'); + passedAnchor.onclick = function() { + setByClassName('fail', 'none'); + setByClassName('pass', 'block'); + + passedAnchor.setAttribute("class", 'activeAnchor'); + failedAnchor.setAttribute("class", ''); + }; + failedAnchor.onclick = function() { + setByClassName('fail', 'block'); + setByClassName('pass', 'none'); + passedAnchor.setAttribute("class", ''); + failedAnchor.setAttribute("class", 'activeAnchor'); + }; + allAnchor.onclick = function() { + setByClassName('fail', 'block'); + setByClassName('pass', 'block'); + passedAnchor.setAttribute("class", ''); + failedAnchor.setAttribute("class", ''); + }; + return div; +} + +DygraphsLocalTester.prototype.postResults = function() { + var table = document.createElement("table"); + this.resultsDiv.appendChild(table); + for (var idx = 0; idx < this.results.length; idx++) { + var result = this.results[idx]; + var tr = document.createElement("tr"); + tr.setAttribute("class", result.result ? 'pass' : 'fail'); + + var tdResult = document.createElement("td"); + tdResult.setAttribute("class", "outcome"); + tdResult.innerText = result.result ? 'pass' : 'fail'; + tr.appendChild(tdResult); + + var tdName = document.createElement("td"); + var div = document.createElement("div"); + div.innerText = result.name; + div.onclick = function(name) { + return function() { + var s = name.split("."); + var testCase = s[0]; + var testName = s[1]; + url = window.location.pathname + + "?testCaseName=" + testCase + + "&test=" + testName + + "&command=runTest"; + window.location.href = url; + }; + }(result.name); + div.setAttribute("class", "anchor"); + tdName.appendChild(div); + tr.appendChild(tdName); + + var tdDuration = document.createElement("td"); + tdDuration.innerText = result.duration; + tr.appendChild(tdDuration); + + if (result.e) { + var tdDetails = document.createElement("td"); + div = document.createElement("div"); + div.innerText = "more..."; + div.setAttribute("class", "anchor"); + div.onclick = function(e) { + return function() { + alert(e + "\n" + e.stack); + }; + }(result.e); + tdDetails.appendChild(div); + tr.appendChild(tdDetails); + } + + table.appendChild(tr); + } +} + +DygraphsLocalTester.prototype.run = function() { + var selector = document.getElementById("selector"); + + if (selector != null) { // running a test + var createAttached = function(name, parent) { + var elem = document.createElement(name); + parent.appendChild(elem); + return elem; + } + + var description = createAttached("div", selector); + var list = createAttached("ul", selector); + var parent = list.parentElement; + var createLink = function(parent, text, url) { + var li = createAttached("li", parent); + var a = createAttached("a", li); + a.innerHTML = text; + a.href = url; + } + if (this.tc == null) { + description.innerHTML = "Test cases:"; + var testCases = getAllTestCases(); + createLink(list, "(run all tests)", document.URL + "?command=runAllTests"); + for (var idx in testCases) { + var entryName = testCases[idx].name; + createLink(list, entryName, document.URL + "?testCaseName=" + entryName); + } + } else { + description.innerHTML = "Tests for " + name; + var names = this.tc.getTestNames(); + createLink(list, "Run All Tests", document.URL + "&command=runAllTests"); + for (var idx in names) { + var name = names[idx]; + createLink(list, name, document.URL + "&test=" + name + "&command=runTest"); + } + } + } +} + +DygraphsLocalTester.prototype.start_ = function(tc) { + this.startms_ = new Date().getTime(); +} + +DygraphsLocalTester.prototype.finish_ = function(tc, name, result, e) { + var endms_ = new Date().getTime(); + this.results.push({ + name : tc.name + "." + name, + result : result, + duration : endms_ - this.startms_, + e : e + }); + this.summary.passed += result ? 1 : 0; + this.summary.failed += result ? 0 : 1; +} diff --git a/auto_tests/tests/callback.js b/auto_tests/tests/callback.js index a727fa5..a8d5e7e 100644 --- a/auto_tests/tests/callback.js +++ b/auto_tests/tests/callback.js @@ -611,3 +611,78 @@ CallbackTestCase.prototype.underlayCallback_yAxisRange = function() { assertEquals(0, yMin); assertEquals(10, yMax); }; + +/** + * Test that drawPointCallback is called for isolated points and correct idx for the point is returned. + */ +CallbackTestCase.prototype.testDrawPointCallback_idx = function() { + var indices = []; + + var g; + var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) { + indices.push(idx); + Dygraph.Circles.DEFAULT.apply(this, arguments); + }; + + var graph = document.getElementById("graph"); + + var testdata = [[10, 2], [11, 3], [12, NaN], [13, 2], [14, NaN], [15, 3]]; + var graphOpts = { + labels: ['X', 'Y'], + valueRange: [0, 4], + drawPoints : false, + drawPointCallback : callback, + pointSize : 8 + }; + + // Test that correct idx for isolated points are passed to the callback. + g = new Dygraph(graph, testdata, graphOpts); + assertEquals(2, indices.length); + assertEquals([3, 5],indices); + + // Test that correct indices for isolated points + gap points are passed to the callback when + // drawGapEdgePoints is set. This should add one point at the right + // edge of the segment at x=11, but not at the graph edge at x=10. + indices = []; // Reset for new test + graphOpts.drawGapEdgePoints = true; + g = new Dygraph(graph, testdata, graphOpts); + assertEquals(3, indices.length); + assertEquals([1, 3, 5],indices); + + + //Test that correct indices are passed to the callback when zoomed in. + indices = []; // Reset for new test + graphOpts.dateWindow = [12.5,13.5] + graphOpts.drawPoints = true; + testdata = [[10, 2], [11, 3], [12, 4], [13, 2], [14, 5], [15, 3]]; + g = new Dygraph(graph, testdata, graphOpts); + assertEquals(3, indices.length); + assertEquals([2, 3, 4],indices); +}; + +/** + * Test that the correct idx is returned for the point in the onHiglightCallback. + */ +CallbackTestCase.prototype.testDrawHighlightPointCallback_idx = function() { + var idxToCheck = null; + + var drawHighlightPointCallback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) { + idxToCheck = idx; + }; + var testdata = [[1, 2], [2, 3], [3, NaN], [4, 2], [5, NaN], [6, 3]]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, testdata, + { + drawHighlightPointCallback : drawHighlightPointCallback + }); + + assertNull(idxToCheck); + DygraphOps.dispatchMouseMove(g, 3, 0); + // check that NaN point is not highlighted + assertNull(idxToCheck); + DygraphOps.dispatchMouseMove(g, 1, 2); + // check that correct index is returned + assertEquals(0,idxToCheck); + DygraphOps.dispatchMouseMove(g, 6, 3); + assertEquals(5,idxToCheck); +}; \ No newline at end of file diff --git a/auto_tests/tests/connect_separated_points.js b/auto_tests/tests/connect_separated_points.js new file mode 100644 index 0000000..4bfe09c --- /dev/null +++ b/auto_tests/tests/connect_separated_points.js @@ -0,0 +1,353 @@ +/** + * @fileoverview Test cases for the option "connectSeparatedPoints" especially for the scenario where not every series has a value for each timestamp. + * + * @author julian.eichstaedt@ch.sauter-bc.com (Fr. Sauter AG) + */ +var ConnectSeparatedPointsTestCase = TestCase("connect-separated-points"); + +ConnectSeparatedPointsTestCase.prototype.setUp = function() { + document.body.innerHTML = "

"; +}; + +ConnectSeparatedPointsTestCase.origFunc = Dygraph.getContext; + +ConnectSeparatedPointsTestCase.prototype.setUp = function() { + document.body.innerHTML = "
"; + Dygraph.getContext = function(canvas) { + return new Proxy(ConnectSeparatedPointsTestCase.origFunc(canvas)); + }; +}; + +ConnectSeparatedPointsTestCase.prototype.tearDown = function() { + Dygraph.getContext = ConnectSeparatedPointsTestCase.origFunc; +}; + +ConnectSeparatedPointsTestCase.prototype.testEdgePointsSimple = function() { + var opts = { + width: 480, + height: 320, + labels: ["x", "series1", "series2", "additionalSeries"], + connectSeparatedPoints: true, + dateWindow: [2.5,7.5] + }; + + var data = [ + [0,-1,0,null], + [1,null,2,null], + [2,null,4,null], + [3,0.5,0,null], + [4,1,-1,5], + [5,2,-2,6], + [6,2.5,-2.5,7], + [7,3,-3,null], + [8,4,null,null], + [9,4,-10,null] + ]; + + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + htx = g.hidden_ctx_; + + var attrs = {}; + + // Test if series1 is drawn correctly. + // ------------------------------------ + + // The first point of the first series + var x1 = data[0][0]; + var y1 = data[0][1]; + var xy1 = g.toDomCoords(x1, y1); + + // The next valid point of this series + var x2 = data[3][0]; + var y2 = data[3][1]; + var xy2 = g.toDomCoords(x2, y2); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if series2 is drawn correctly. + // ------------------------------------ + + // The last point of the second series. + var x2 = data[9][0]; + var y2 = data[9][2]; + var xy2 = g.toDomCoords(x2, y2); + + // The previous valid point of this series + var x1 = data[7][0]; + var y1 = data[7][2]; + var xy1 = g.toDomCoords(x1, y1); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); +}; + +ConnectSeparatedPointsTestCase.prototype.testEdgePointsCustomBars = function() { + var opts = { + width: 480, + height: 320, + labels: ["x", "series1", "series2", "additionalSeries"], + connectSeparatedPoints: true, + dateWindow: [2.5,7.5], + customBars: true + }; + + var data = [ + [0,[4,5,6], [1,2,3], [null, null, null]], + [1,[null,null,null], [2,3,4], [null, null, null]], + [2,[null,null,null], [3,4,5], [null, null, null]], + [3,[0,1,2], [2,3,4], [null, null, null]], + [4,[1,2,3], [2,3,4], [4, 5, 6]], + [5,[1,2,3], [3,4,5], [4, 5, 6]], + [6,[0,1,2], [4,5,6], [5, 6, 7]], + [7,[0,1,2], [4,5,6], [null, null, null]], + [8,[2,3,4], [null,null,null], [null, null, null]], + [9,[0,1,2], [2,4,9], [null, null, null]] + + ]; + + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + htx = g.hidden_ctx_; + + var attrs = {}; + + + // Test if values of the series1 are drawn correctly. + // ------------------------------------ + + // The first point of the first series + var x1 = data[0][0]; + var y1 = data[0][1][1]; + var xy1 = g.toDomCoords(x1, y1); + + // The next valid point of this series + var x2 = data[3][0]; + var y2 = data[3][1][1]; + var xy2 = g.toDomCoords(x2, y2); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the custom bars of the series1 are drawn correctly + // -------------------------------------------- + + // The first min-point of this series + x1 = data[0][0]; + y1 = data[0][1][0]; + xy1 = g.toDomCoords(x1, y1); + + // The next valid min-point of the second series. + x2 = data[3][0]; + y2 = data[3][1][0]; + xy2 = g.toDomCoords(x2, y2); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // The first max-point of this series + x1 = data[0][0]; + y1 = data[0][1][2]; + xy1 = g.toDomCoords(x1, y1); + + // The next valid max-point of the second series. + x2 = data[3][0]; + y2 = data[3][1][2]; + xy2 = g.toDomCoords(x2, y2); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if values of the series2 are drawn correctly. + // ------------------------------------ + + // The last point of the second series. + var x2 = data[9][0]; + var y2 = data[9][2][1]; + var xy2 = g.toDomCoords(x2, y2); + + // The previous valid point of this series + var x1 = data[7][0]; + var y1 = data[7][2][1]; + var xy1 = g.toDomCoords(x1, y1); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the custom bars of the series2 are drawn correctly + // -------------------------------------------- + + // The last min-point of the second series. + x2 = data[9][0]; + y2 = data[9][2][0]; + xy2 = g.toDomCoords(x2, y2); + + // The previous valid min-point of this series + x1 = data[7][0]; + y1 = data[7][2][0]; + xy1 = g.toDomCoords(x1, y1); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // The last max-point of the second series. + x2 = data[9][0]; + y2 = data[9][2][2]; + xy2 = g.toDomCoords(x2, y2); + + // The previous valid max-point of this series + x1 = data[7][0]; + y1 = data[7][2][2]; + xy1 = g.toDomCoords(x1, y1); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); +}; + +ConnectSeparatedPointsTestCase.prototype.testEdgePointsErrorBars = function() { + var opts = { + width: 480, + height: 320, + labels: ["x", "series1", "series2", "seriesTestHelper"], + connectSeparatedPoints: true, + dateWindow: [2,7.5], + errorBars: true + + }; + + var data = [ + [0,[5,1], [2,1], [null,null]], + [1,[null,null], [3,1], [null,null]], + [2,[null,null], [4,1], [null,null]], + [3,[1,1], [3,1], [null,null]], + [4,[2,1], [3,1], [5,1]], + [5,[2,1], [4,1], [5,1]], + [6,[1,1], [5,1], [6,1]], + [7,[1,1], [5,1], [null,null]], + [8,[3,1], [null,null], [null,null]], + [9,[1,1], [4,1], [null,null]] + + ]; + + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + htx = g.hidden_ctx_; + + var attrs = {}; + + + // Test if values of the series1 are drawn correctly. + // ------------------------------------ + + // The first point of the first series + var x1 = data[0][0]; + var y1 = data[0][1][0]; + var xy1 = g.toDomCoords(x1, y1); + + // The next valid point of this series + var x2 = data[3][0]; + var y2 = data[3][1][0]; + var xy2 = g.toDomCoords(x2, y2); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the upper error bars of series1 are drawn correctly + // -------------------------------------------- + + // The first upper error-point of this series + x1 = data[0][0]; + var y1error = y1 + (data[0][1][1]*2); + xy1 = g.toDomCoords(x1, y1error); + + // The next valid upper error-point of the second series. + x2 = data[3][0]; + var y2error = y2 + (data[3][1][1]*2); + xy2 = g.toDomCoords(x2, y2error); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the lower error bars of series1 are drawn correctly + // -------------------------------------------- + + // The first lower error-point of this series + x1 = data[0][0]; + y1error = y1 - (data[0][1][1]*2); + xy1 = g.toDomCoords(x1, y1error); + + //The next valid lower error-point of the second series. + x2 = data[3][0]; + y2error = y2 - (data[3][1][1]*2); + xy2 = g.toDomCoords(x2, y2error); + + // Check if both points are connected at the left edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + + // Test if values of the series2 are drawn correctly. + // ------------------------------------ + + // The last point of this series + x2 = data[9][0]; + y2 = data[9][2][0]; + xy2 = g.toDomCoords(x2, y2); + + // The previous valid point of the first series + x1 = data[7][0]; + y1 = data[7][2][0]; + xy1 = g.toDomCoords(x1, y1); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the upper error bars of series2 are drawn correctly + // -------------------------------------------- + + // The last upper error-point of the second series. + x2 = data[9][0]; + var y2error = y2 + (data[9][2][1]*2); + xy2 = g.toDomCoords(x2, y2error); + + // The previous valid upper error-point of this series + x1 = data[7][0]; + var y1error = y1 + (data[7][2][1]*2); + xy1 = g.toDomCoords(x1, y1error); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); + + // Test if the lower error bars of series1 are drawn correctly + // -------------------------------------------- + + // The last lower error-point of the second series. + x2 = data[9][0]; + y2error = y2 - (data[9][2][1]*2); + xy2 = g.toDomCoords(x2, y2error); + + // The previous valid lower error-point of this series + x1 = data[7][0]; + y1error = y1 - (data[7][2][1]*2); + xy1 = g.toDomCoords(x1, y1error); + + // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly + // even if the point is outside the visible range and only one series has a valid value for this point. + CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); +}; diff --git a/auto_tests/tests/dygraph-options-tests.js b/auto_tests/tests/dygraph-options-tests.js new file mode 100644 index 0000000..691b24a --- /dev/null +++ b/auto_tests/tests/dygraph-options-tests.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Test cases for DygraphOptions. + */ +var DygraphOptionsTestCase = TestCase("dygraph-options-tests"); + +DygraphOptionsTestCase.prototype.setUp = function() { + document.body.innerHTML = "
"; +}; + +DygraphOptionsTestCase.prototype.tearDown = function() { +}; + +/* + * Pathalogical test to ensure getSeriesNames works + */ +DygraphOptionsTestCase.prototype.testGetSeriesNames = function() { + var opts = { + width: 480, + height: 320 + }; + var data = "X,Y,Y2,Y3\n" + + "0,-1,0,0"; + + // Kind of annoying that you need a DOM to test the object. + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + // We don't need to get at g's attributes_ object just + // to test DygraphOptions. + var o = new DygraphOptions(g); + assertEquals(["Y", "Y2", "Y3"], o.seriesNames()); +}; diff --git a/auto_tests/tests/error_bars.js b/auto_tests/tests/error_bars.js index 7e50e42..1e9d0f4 100644 --- a/auto_tests/tests/error_bars.js +++ b/auto_tests/tests/error_bars.js @@ -86,6 +86,7 @@ errorBarsTestCase.prototype.testErrorBarsDrawn = function() { xy2 = g.toDomCoords(data[i + 1][0], data[i + 1][1][1]); CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs); } + g.destroy(); // Restore balanced saves and restores. CanvasAssertions.assertBalancedSaveRestore(htx); }; diff --git a/auto_tests/tests/grid_per_axis.js b/auto_tests/tests/grid_per_axis.js new file mode 100644 index 0000000..e5c960f --- /dev/null +++ b/auto_tests/tests/grid_per_axis.js @@ -0,0 +1,317 @@ +/** + * @fileoverview Test cases for the per-axis grid options, including the new + * option "gridLinePattern". + * + * @author david.eberlein@ch.sauter-bc.com (Fr. Sauter AG) + */ +var GridPerAxisTestCase = TestCase("grid-per-axis"); + +GridPerAxisTestCase.prototype.setUp = function() { + document.body.innerHTML = "
"; +}; + +GridPerAxisTestCase.origFunc = Dygraph.getContext; + +GridPerAxisTestCase.prototype.setUp = function() { + document.body.innerHTML = "
"; + Dygraph.getContext = function(canvas) { + return new Proxy(GridPerAxisTestCase.origFunc(canvas)); + }; +}; + +GridPerAxisTestCase.prototype.tearDown = function() { + Dygraph.getContext = GridPerAxisTestCase.origFunc; +}; + +GridPerAxisTestCase.prototype.testIndependentGrids = function() { + var opts = { + width : 480, + height : 320, + errorBars : false, + labels : [ "X", "Left", "Right" ], + series : { + Left : { + axis : "y" + }, + Right : { + axis : "y2" + } + }, + axes : { + y2 : { + drawGrid : true, + independentTicks : true + } + } + }; + + var data = [ [ 1, 0, 0 ], [ 2, 12, 88 ], [ 3, 88, 122 ], [ 4, 63, 273 ], + [ 5, 110, 333 ] ]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + htx = g.hidden_ctx_; + + // The expected gridlines + var yGridlines = [ 0, 20, 40, 60, 80, 100, 120 ]; + var y2Gridlines = [ 0, 50, 100, 150, 200, 250, 300, 350 ]; + var gridlines = [ yGridlines, y2Gridlines ]; + + function halfUp(x) { + return Math.round(x) + 0.5; + } + function halfDown(y) { + return Math.round(y) - 0.5; + } + + var attrs = {}, x, y; + x = halfUp(g.plotter_.area.x); + // Step through y(0) and y2(1) axis + for ( var axis = 0; axis < 2; axis++) { + // Step through all gridlines of the axis + for ( var i = 0; i < gridlines[axis].length; i++) { + // Check the labels: + var labels = Util.getYLabels(axis + 1); + assertEquals("Expected label not found.", gridlines[axis][i], labels[i]); + + // Check that the grid was drawn. + y = halfDown(g.toDomYCoord(gridlines[axis][i], axis)); + var p1 = [ x, y ]; + var p2 = [ x + g.plotter_.area.w, y ]; + CanvasAssertions.assertLineDrawn(htx, p1, p2, attrs); + } + } +}; + +GridPerAxisTestCase.prototype.testPerAxisGridColors = function() { + var opts = { + width : 480, + height : 320, + errorBars : false, + labels : [ "X", "Left", "Right" ], + series : { + Left : { + axis : "y" + }, + Right : { + axis : "y2" + } + }, + axes : { + y : { + gridLineColor : "#0000ff", + gridLineWidth : 2 + }, + y2 : { + drawGrid : true, + independentTicks : true, + gridLineColor : "#ff0000", + gridLineWidth : 2, + } + } + }; + var data = [ [ 1, 0, 0 ], [ 2, 12, 88 ], [ 3, 88, 122 ], [ 4, 63, 273 ], + [ 5, 110, 333 ] ]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + htx = g.hidden_ctx_; + + // The expected gridlines + var yGridlines = [ 20, 40, 60, 80, 100, 120 ]; + var y2Gridlines = [ 50, 100, 150, 200, 250, 300, 350 ]; + var gridlines = [ yGridlines, y2Gridlines ]; + var gridColors = [ [ 0, 0, 255, 255 ], [ 255, 0, 0, 255 ] ]; + + function halfUp(x) { + return Math.round(x) + 1; + } + function halfDown(y) { + return Math.round(y) - 1; + } + var x, y; + x = halfUp(g.plotter_.area.x); + // Step through y(0) and y2(1) axis + for ( var axis = 0; axis < 2; axis++) { + // Step through all gridlines of the axis + for ( var i = 0; i < gridlines[axis].length; i++) { + y = halfDown(g.toDomYCoord(gridlines[axis][i], axis)); + // Check the grid colors. + assertEquals("Unexpected grid color found at pixel: x: " + x + "y: " + y, + gridColors[axis], Util.samplePixel(g.hidden_, x, y)); + } + } +}; +GridPerAxisTestCase.prototype.testPerAxisGridWidth = function() { + var opts = { + width : 480, + height : 320, + errorBars : false, + gridLineColor : "#ff0000", + labels : [ "X", "Left", "Right" ], + series : { + Left : { + axis : "y" + }, + Right : { + axis : "y2" + } + }, + axes : { + x : { + gridLineWidth : 4 + }, + y : { + gridLineWidth : 2 + }, + y2 : { + drawGrid : true, + independentTicks : true, + gridLineWidth : 1 + } + } + }; + var data = [ [ 1, 0, 0 ], [ 2, 12, 88 ], [ 3, 88, 122 ], [ 4, 63, 273 ], + [ 5, 110, 333 ] ]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + htx = g.hidden_ctx_; + + // The expected gridlines + var yGridlines = [ 20, 40, 60, 80 ]; + var y2Gridlines = [ 50, 100, 150, 200, 250, 350 ]; + var gridlines = [ yGridlines, y2Gridlines ]; + var xGridlines = [ 2, 3, 4 ]; + var gridColor = [ 255, 0, 0 ]; + var emptyColor = [ 0, 0, 0 ]; + + function halfUp(x) { + return Math.round(x) + 1; + } + function halfDown(y) { + return Math.round(y) - 1; + } + var x, y; + x = halfUp(g.plotter_.area.x + 10); + // Step through y(0) and y2(1) axis + for ( var axis = 0; axis < 2; axis++) { + // Step through all gridlines of the axis + for ( var i = 0; i < gridlines[axis].length; i++) { + y = halfDown(g.toDomYCoord(gridlines[axis][i], axis)); + // Ignore the alpha value + var drawnPixeldown2 = Util.samplePixel(g.hidden_, x, y - 2).slice(0, 3); + var drawnPixeldown1 = Util.samplePixel(g.hidden_, x, y - 1).slice(0, 3); + var drawnPixel = Util.samplePixel(g.hidden_, x, y).slice(0, 3); + var drawnPixelup1 = Util.samplePixel(g.hidden_, x, y + 1).slice(0, 3); + var drawnPixelup2 = Util.samplePixel(g.hidden_, x, y + 2).slice(0, 3); + // Check the grid width. + switch (axis) { + case 0: // y with 2 pixels width + assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: " + + y, emptyColor, drawnPixeldown2); + assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: " + + y, gridColor, drawnPixeldown1); + assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: " + + y, gridColor, drawnPixel); + assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: " + + y, gridColor, drawnPixelup1); + assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: " + + y, emptyColor, drawnPixelup2); + break; + case 1: // y2 with 1 pixel width + assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: " + + y, emptyColor, drawnPixeldown1); + assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: " + + y, gridColor, drawnPixel); + assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: " + + y, emptyColor, drawnPixelup1); + break; + } + } + } + + // Check the x axis grid + y = halfDown(g.plotter_.area.y) + 10; + for ( var i = 0; i < xGridlines.length; i++) { + x = halfUp(g.toDomXCoord(xGridlines[i])); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + emptyColor, Util.samplePixel(g.hidden_, x - 4, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + gridColor, Util.samplePixel(g.hidden_, x - 3, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + gridColor, Util.samplePixel(g.hidden_, x - 2, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + gridColor, Util.samplePixel(g.hidden_, x - 1, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + gridColor, Util.samplePixel(g.hidden_, x, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + gridColor, Util.samplePixel(g.hidden_, x + 1, y).slice(0, 3)); + assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y, + emptyColor, Util.samplePixel(g.hidden_, x + 2, y).slice(0, 3)); + } +}; +GridPerAxisTestCase.prototype.testGridLinePattern = function() { + var opts = { + width : 480, + height : 320, + errorBars : false, + drawXGrid : false, + drawXAxis : false, + drawYAxis : false, + labels : [ "X", "Left", "Right" ], + colors : [ "rgba(0,0,0,0)", "rgba(0,0,0,0)" ], + series : { + Left : { + axis : "y" + }, + Right : { + axis : "y2" + } + }, + axes : { + y : { + gridLineColor : "#0000ff", + gridLinePattern : [ 10, 10 ] + } + } + }; + var data = [ [ 1, 0, 0 ], [ 2, 12, 88 ], [ 3, 88, 122 ], [ 4, 63, 273 ], + [ 5, 110, 333 ] ]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + htx = g.hidden_ctx_; + + // The expected gridlines + var yGridlines = [ 0, 20, 40, 60, 80, 100, 120 ]; + + function halfUp(x) { + return Math.round(x) + 1; + } + function halfDown(y) { + return Math.round(y) - 1; + } + var x, y; + // Step through all gridlines of the axis + for ( var i = 0; i < yGridlines.length; i++) { + y = halfDown(g.toDomYCoord(yGridlines[i], 0)); + // Step through the pixels of the line and test the pattern. + for (x = halfUp(g.plotter_.area.x); x < g.plotter_.area.w; x++) { + // avoid checking the edge pixels since they differ depending on the OS. + var pixelpos = x % 10; + if(pixelpos < 1 || pixelpos > 8) continue; + + // Ignore alpha + var drawnPixel = Util.samplePixel(g.hidden_, x, y).slice(0,3); + var pattern = (Math.floor((x) / 10)) % 2; + switch (pattern) { + case 0: // fill + assertEquals("Unexpected filled grid-pattern color found at pixel: x: " + x + "y: " + + y, [ 0, 0, 255 ], drawnPixel); + break; + case 1: // no fill + assertEquals("Unexpected empty grid-pattern color found at pixel: x: " + x + "y: " + + y, [ 0, 0, 0 ], drawnPixel); + break; + } + } + } +}; diff --git a/auto_tests/tests/missing_points.js b/auto_tests/tests/missing_points.js index 68a5024..3c52dd0 100644 --- a/auto_tests/tests/missing_points.js +++ b/auto_tests/tests/missing_points.js @@ -50,7 +50,7 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw = function() { 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"); @@ -69,7 +69,7 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded = function( { strokeStyle: '#0000ff', }); CanvasAssertions.assertLineDrawn(htx, [370, 87], [475, 25], { strokeStyle: '#0000ff', }); -} +}; MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded_connected = function() { var graph = document.getElementById("graph"); @@ -88,7 +88,7 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded_connected = 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. @@ -122,7 +122,7 @@ MissingPointsTestCase.prototype.testConnectSeparatedPoints = function() { 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. @@ -158,7 +158,7 @@ MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() { CanvasAssertions.assertConsecutiveLinesDrawn(htx, [[56, 244], [149, 181], [242, 118]], { strokeStyle: '#0000ff' }); -} +}; /* These lines contain awesome powa! var lines = CanvasAssertions.getLinesDrawn(htx, {strokeStyle: "#0000ff"}); @@ -167,3 +167,143 @@ MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() { console.log(line[0].args, line[1].args, line[0].properties.strokeStyle); } */ + +MissingPointsTestCase.prototype.testErrorBarsWithMissingPoints = function() { + var data = [ + [1, [2,1]], + [2, [3,1]], + [3, null], + [4, [5,1]], + [5, [4,1]], + [6, [null,null]], + ]; + var g = new Dygraph( + document.getElementById("graph"), + data, + { + errorBars: true, + colors: ['red'] + } + ); + + var htx = g.hidden_ctx_; + + assertEquals(8, CanvasAssertions.numLinesDrawn(htx, '#ff0000')); + + var p0 = g.toDomCoords(data[0][0], data[0][1][0]); + var p1 = g.toDomCoords(data[1][0], data[1][1][0]); + var p2 = g.toDomCoords(data[3][0], data[3][1][0]); + var p3 = g.toDomCoords(data[4][0], data[4][1][0]); + CanvasAssertions.assertConsecutiveLinesDrawn(htx, + [p0, p1], { strokeStyle: '#ff0000' }); + CanvasAssertions.assertConsecutiveLinesDrawn(htx, + [p2, p3], { strokeStyle: '#ff0000' }); +}; + +MissingPointsTestCase.prototype.testErrorBarsWithMissingPointsConnected = function() { + var data = [ + [1, [null,1]], + [2, [2,1]], + [3, null], + [4, [5,1]], + [5, [null,null]], + [6, [3,1]] + ]; + var g = new Dygraph( + document.getElementById("graph"), + data, + { + connectSeparatedPoints: true, + drawPoints: true, + errorBars: true, + colors: ['red'] + } + ); + + var htx = g.hidden_ctx_; + + assertEquals(8, CanvasAssertions.numLinesDrawn(htx, '#ff0000')); + + var p1 = g.toDomCoords(data[1][0], data[1][1][0]); + var p2 = g.toDomCoords(data[3][0], data[3][1][0]); + var p3 = g.toDomCoords(data[5][0], data[5][1][0]); + CanvasAssertions.assertConsecutiveLinesDrawn(htx, + [p1, p2, p3], + { strokeStyle: '#ff0000' }); +}; +MissingPointsTestCase.prototype.testCustomBarsWithMissingPoints = function() { + var data = [ + [1, [1,2,3]], + [2, [2,3,4]], + [3, null], + [4, [4,5,6]], + [5, [3,4,5]], + [6, [null,null,null]], + [7, [2,3,4]], + [8, [1,2,3]], + [9, NaN], + [10, [2,3,4]], + [11, [3,4,5]], + [12, [NaN,NaN,NaN]] + ]; + var g = new Dygraph( + document.getElementById("graph"), + data, + { + customBars: true, + colors: ['red'] + } + ); + + var htx = g.hidden_ctx_; + + assertEquals(16, CanvasAssertions.numLinesDrawn(htx, '#ff0000')); + + var p0 = g.toDomCoords(data[0][0], data[0][1][1]); + var p1 = g.toDomCoords(data[1][0], data[1][1][1]); + CanvasAssertions.assertLineDrawn(htx, p0, p1, { strokeStyle: '#ff0000' }); + + p0 = g.toDomCoords(data[3][0], data[3][1][1]); + p1 = g.toDomCoords(data[4][0], data[4][1][1]); + CanvasAssertions.assertLineDrawn(htx, p0, p1, { strokeStyle: '#ff0000' }); + + p0 = g.toDomCoords(data[6][0], data[6][1][1]); + p1 = g.toDomCoords(data[7][0], data[7][1][1]); + CanvasAssertions.assertLineDrawn(htx, p0, p1, { strokeStyle: '#ff0000' });; + + p0 = g.toDomCoords(data[9][0], data[9][1][1]); + p1 = g.toDomCoords(data[10][0], data[10][1][1]); + CanvasAssertions.assertLineDrawn(htx, p0, p1, { strokeStyle: '#ff0000' }); +}; + +MissingPointsTestCase.prototype.testCustomBarsWithMissingPointsConnected = function() { + var data = [ + [1, [1,null,1]], + [2, [1,2,3]], + [3, null], + [4, [4,5,6]], + [5, [null,null,null]], + [6, [2,3,4]] + ]; + var g = new Dygraph( + document.getElementById("graph"), + data, + { + connectSeparatedPoints: true, + drawPoints: true, + customBars: true, + colors: ['red'] + } + ); + + var htx = g.hidden_ctx_; + + assertEquals(8, CanvasAssertions.numLinesDrawn(htx, '#ff0000')); + + var p1 = g.toDomCoords(data[1][0], data[1][1][1]); + var p2 = g.toDomCoords(data[3][0], data[3][1][1]); + var p3 = g.toDomCoords(data[5][0], data[5][1][1]); + CanvasAssertions.assertConsecutiveLinesDrawn(htx, + [p1, p2, p3], + { strokeStyle: '#ff0000' }); +}; diff --git a/auto_tests/tests/per_series.js b/auto_tests/tests/per_series.js index ab52add..1a04496 100644 --- a/auto_tests/tests/per_series.js +++ b/auto_tests/tests/per_series.js @@ -156,11 +156,12 @@ perSeriesTestCase.prototype.testOldAxisSpecInNewSeriesThrows = function() { }; var graph = document.getElementById("graph"); var data = "X,A,B,C,D,E\n0,1,2,3,4,5\n"; + var threw = false; try { new Dygraph(graph, data, opts); } catch(e) { - assertEquals( - "Using objects for axis specification is not supported inside the 'series' option.", - e); + threw = true; } + + assertTrue(threw); } diff --git a/auto_tests/tests/range_selector.js b/auto_tests/tests/range_selector.js index a27cad7..34e3574 100644 --- a/auto_tests/tests/range_selector.js +++ b/auto_tests/tests/range_selector.js @@ -318,6 +318,32 @@ RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() { assert(newXRange[1]+'<'+xRange[1], newXRange[1] < xRange[1]); }; + +RangeSelectorTestCase.prototype.testRangeSelectorPositionIfXAxisNotDrawn = function() { + var opts = { + width: 480, + height: 100, + xAxisHeight: 30, + drawXAxis: false, + showRangeSelector: true, + rangeSelectorHeight: 30 + }; + var data = [ + [0, 1], + [10, 1] + ]; + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + + //assert, that the range selector is at top position 70 since the 30px of the + // xAxis shouldn't be reserved since it isn't drawn. + this.assertGraphExistence(g, graph); + var bgcanvas = graph.getElementsByClassName('dygraph-rangesel-bgcanvas')[0]; + assertEquals("Range selector is not at the expected position.","70px", bgcanvas.style.top); + var fgcanvas = graph.getElementsByClassName('dygraph-rangesel-fgcanvas')[0]; + assertEquals("Range selector is not at the expected position.","70px", fgcanvas.style.top); +}; + RangeSelectorTestCase.prototype.assertGraphExistence = function(g, graph) { assertNotNull(g); var zoomhandles = graph.getElementsByClassName('dygraph-rangesel-zoomhandle'); diff --git a/auto_tests/tests/simple_drawing.js b/auto_tests/tests/simple_drawing.js index 75a6e87..27e9a1a 100644 --- a/auto_tests/tests/simple_drawing.js +++ b/auto_tests/tests/simple_drawing.js @@ -55,6 +55,7 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() { strokeStyle: "#008080", lineWidth: 1 }); + g.destroy(); // to balance context saves and destroys. CanvasAssertions.assertBalancedSaveRestore(htx); }; @@ -76,6 +77,7 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangeZeroToFifty = function() { lineWidth: 1 }); assertEquals(1, lines.length); + g.destroy(); // to balance context saves and destroys. CanvasAssertions.assertBalancedSaveRestore(htx); }; @@ -84,6 +86,7 @@ SimpleDrawingTestCase.prototype.testDrawWithAxis = function() { var g = new Dygraph(graph, ZERO_TO_FIFTY); var htx = g.hidden_ctx_; + g.destroy(); // to balance context saves and destroys. CanvasAssertions.assertBalancedSaveRestore(htx); }; @@ -110,6 +113,7 @@ SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() { // TODO(danvk): figure out a good way to restore this test. // assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000")); + g.destroy(); // to balance context saves and destroys. CanvasAssertions.assertBalancedSaveRestore(htx); }; @@ -146,5 +150,6 @@ SimpleDrawingTestCase.prototype.testDrawThickLine = function() { // TODO(danvk): figure out a good way to restore this test. // assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000")); + g.destroy(); // to balance context saves and destroys. CanvasAssertions.assertBalancedSaveRestore(htx); }; diff --git a/auto_tests/tests/step_plot_per_series.js b/auto_tests/tests/step_plot_per_series.js index bf25948..d1bf323 100644 --- a/auto_tests/tests/step_plot_per_series.js +++ b/auto_tests/tests/step_plot_per_series.js @@ -3,7 +3,7 @@ * * @author julian.eichstaedt@ch.sauter-bc.com (Fr. Sauter AG) */ -var StepTestCase = TestCase("step_plot_per_series"); +var StepTestCase = TestCase("step-plot-per-series"); StepTestCase.prototype.setUp = function() { document.body.innerHTML = "
"; diff --git a/closure-todo.txt b/closure-todo.txt index c9d42b7..5c61495 100644 --- a/closure-todo.txt +++ b/closure-todo.txt @@ -12,7 +12,7 @@ Core: - dygraph-canvas.js - dygraph-interaction-model.js - dygraph-layout.js -- dygraph-options.js +x dygraph-options.js - dygraph-range-selector.js - dygraph.js x dygraph-gviz.js @@ -32,6 +32,6 @@ Plugins: Here's a command that can be used to build dygraphs using the closure compiler: -java -jar ../../closure-compiler-read-only/build/compiler.jar --js=dygraph-utils.js --js=dashed-canvas.js --js=dygraph-options-reference.js --js=dygraph-tickers.js --js=dygraph-gviz.js --js_output_file=/tmp/out.js --compilation_level ADVANCED_OPTIMIZATIONS --warning_level VERBOSE --externs dygraph-externs.js +java -jar ../../closure-compiler-read-only/build/compiler.jar --js=dygraph-utils.js --js=dashed-canvas.js --js=dygraph-options-reference.js --js=dygraph-tickers.js --js=dygraph-gviz.js --js=dygraph-options.js --js_output_file=/tmp/out.js --compilation_level ADVANCED_OPTIMIZATIONS --warning_level VERBOSE --externs dygraph-externs.js As each file is closurized, it can be added as a "--js" parameter. diff --git a/docs/index.html b/docs/index.html index 7c1dcdc..cc3d1be 100644 --- a/docs/index.html +++ b/docs/index.html @@ -93,7 +93,13 @@

dygraphs JavaScript Visualization Library

http://github.com/danvk/dygraphs

-

See blog, mailing list, downloads, demos and open issues

+

For help, ask + a question on StackOverflow. You may also be interested in the blog, mailing list, demos and open issues.

dygraphs is an open source JavaScript library that produces produces interactive, zoomable charts of time series. It is designed to display dense data sets and enable users to explore and interpret them.

diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 99ad493..ef6cf1f 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -39,8 +39,8 @@ * The chart canvas has already been created by the Dygraph object. The * renderer simply gets a drawing context. * - * @param {Dyraph} dygraph The chart to which this renderer belongs. - * @param {Canvas} element The <canvas> DOM element on which to draw. + * @param {Dygraph} dygraph The chart to which this renderer belongs. + * @param {HTMLCanvasElement} element The <canvas> DOM element on which to draw. * @param {CanvasRenderingContext2D} elementContext The drawing context. * @param {DygraphLayout} layout The chart's DygraphLayout object. * @@ -58,6 +58,7 @@ var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { this.width = this.element.width; // --- check whether everything is ok before we return + // NOTE(konigsberg): isIE is never defined in this object. Bug of some sort. if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element))) throw "Canvas is not supported."; @@ -373,7 +374,7 @@ DygraphCanvasRenderer._drawSeries = function(e, ctx.moveTo(point.canvasx, point.canvasy); } if (drawPoints || isIsolated) { - pointsOnLine.push([point.canvasx, point.canvasy]); + pointsOnLine.push([point.canvasx, point.canvasy, point.idx]); } prevCanvasX = point.canvasx; prevCanvasY = point.canvasy; @@ -398,7 +399,7 @@ DygraphCanvasRenderer._drawPointsOnLine = function( var cb = pointsOnLine[idx]; ctx.save(); drawPointCallback( - e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize); + e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]); ctx.restore(); } }; @@ -433,14 +434,16 @@ DygraphCanvasRenderer.prototype._updatePoints = function() { /** * Add canvas Actually draw the lines chart, including error bars. - * If opt_seriesName is specified, only that series will be drawn. - * (This is used for expedited redrawing with highlightSeriesOpts) - * Lines are typically drawn in the non-interactive dygraph canvas. If opt_ctx - * is specified, they can be drawn elsewhere. * * This function can only be called if DygraphLayout's points array has been * updated with canvas{x,y} attributes, i.e. by * DygraphCanvasRenderer._updatePoints. + * + * @param {string=} opt_seriesName when specified, only that series will + * be drawn. (This is used for expedited redrawing with highlightSeriesOpts) + * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing + * context. However, lines are typically drawn on the object's + * elementContext. * @private */ DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) { diff --git a/dygraph-externs.js b/dygraph-externs.js index 6e487fd..ed5de4c 100644 --- a/dygraph-externs.js +++ b/dygraph-externs.js @@ -72,13 +72,22 @@ function DygraphLayout() {} */ DygraphLayout.prototype.datasets; +// TODO: DygraphOptions should not reach inside Dygraph private data like this. +/** @type {Object} */ +Dygraph.prototype.attrs_; +/** @type {Object} */ +Dygraph.prototype.user_attrs_; + /** * @type {DygraphLayout} */ Dygraph.prototype.layout_; +/** @type {function(): string} */ +Dygraph.prototype.getHighlightSeries; + /** @type {Array.<{elem:Element,type:string,fn:function(!Event):(boolean|undefined|null)}>} */ Dygraph.prototype.registeredEvents_; -/** @type {Object} */ +/** @type {{axes: Object}} */ Dygraph.DEFAULT_ATTRS; diff --git a/dygraph-layout.js b/dygraph-layout.js index 4caafc0..4b600f7 100644 --- a/dygraph-layout.js +++ b/dygraph-layout.js @@ -52,6 +52,12 @@ DygraphLayout.prototype.addDataset = function(setname, set_xy) { this.setNames.push(setname); }; +/** + * Returns the box which the chart should be drawn in. This is the canvas's + * box, less space needed for the axis and chart labels. + * + * @return {{x: number, y: number, w: number, h: number}} + */ DygraphLayout.prototype.getPlotArea = function() { return this.area_; }; @@ -211,6 +217,7 @@ DygraphLayout.prototype._evaluateLineCharts = function() { // on chrome+linux, they are 6 times more expensive than iterating through the // points and drawing the lines. The brunt of the cost comes from allocating // the |point| structures. + var boundaryIdStart = this.dygraph_.getLeftBoundary_(); for (var setIdx = 0; setIdx < this.datasets.length; setIdx++) { var dataset = this.datasets[setIdx]; var setName = this.setNames[setIdx]; @@ -242,7 +249,8 @@ DygraphLayout.prototype._evaluateLineCharts = function() { y: yNormal, xval: xValue, yval: yValue, - name: setName // TODO(danvk): is this really necessary? + name: setName, // TODO(danvk): is this really necessary? + idx: j + boundaryIdStart }; } diff --git a/dygraph-options-reference.js b/dygraph-options-reference.js index 38fefcc..bd69bfc 100644 --- a/dygraph-options-reference.js +++ b/dygraph-options-reference.js @@ -60,7 +60,8 @@ Dygraph.OPTIONS_REFERENCE = // [ "cx" , "center x coordinate" ], [ "cy" , "center y coordinate" ], [ "color" , "series color" ], - [ "pointSize" , "the radius of the image." ] + [ "pointSize" , "the radius of the image." ], + [ "idx" , "the row-index of the point in the data."] ], "description": "Draw a custom item when drawPoints is enabled. Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy). Also see drawHighlightPointCallback" }, @@ -129,7 +130,8 @@ Dygraph.OPTIONS_REFERENCE = // [ "cx" , "center x coordinate" ], [ "cy" , "center y coordinate" ], [ "color" , "series color" ], - [ "pointSize" , "the radius of the image." ] + [ "pointSize" , "the radius of the image." ], + [ "idx" , "the row-index of the point in the data."] ], "description": "Draw a custom item when a point is highlighted. Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy) Also see drawPointCallback" }, @@ -408,7 +410,7 @@ Dygraph.OPTIONS_REFERENCE = // "default": "rgb(128,128,128)", "labels": ["Grid"], "type": "red, blue", - "description": "The color of the gridlines." + "description": "The color of the gridlines. This may be set on a per-axis basis to define each axis' grid separately." }, "visibility": { "default": "[true, true, ...]", @@ -648,15 +650,27 @@ Dygraph.OPTIONS_REFERENCE = // }, "drawXGrid": { "default": "true", - "labels": ["Grid"], + "labels": ["Grid","Deprecated"], "type": "boolean", - "description" : "Whether to display vertical gridlines under the chart." + "description" : "Use the per-axis option drawGrid instead. Whether to display vertical gridlines under the chart." }, "drawYGrid": { "default": "true", + "labels": ["Grid","Deprecated"], + "type": "boolean", + "description" : "Use the per-axis option drawGrid instead. Whether to display horizontal gridlines under the chart." + }, + "drawGrid": { + "default": "true for x and y, false for y2", "labels": ["Grid"], "type": "boolean", - "description" : "Whether to display horizontal gridlines under the chart." + "description" : "Whether to display gridlines in the chart. This may be set on a per-axis basis to define the visibility of each axis' grid separately." + }, + "independentTicks": { + "default": "true for y, false for y2", + "labels": ["Axis display", "Grid"], + "type": "boolean", + "description" : "Only valid for y and y2, has no effect on x: This option defines whether the y axes should align their ticks or if they should be independent. Possible combinations: 1.) y=true, y2=false (default): y is the primary axis and the y2 ticks are aligned to the the ones of y. (only 1 grid) 2.) y=false, y2=true: y2 is the primary axis and the y ticks are aligned to the the ones of y2. (only 1 grid) 3.) y=true, y2=true: Both axis are independent and have their own ticks. (2 grids) 4.) y=false, y2=false: Invalid configuration causes an error." }, "drawXAxis": { "default": "true", @@ -674,7 +688,7 @@ Dygraph.OPTIONS_REFERENCE = // "default": "0.3", "labels": ["Grid"], "type": "float", - "description" : "Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal gridlines can be turned off entirely by using the drawXGrid and drawYGrid options." + "description" : "Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal gridlines can be turned off entirely by using the drawXGrid and drawYGrid options. This may be set on a per-axis basis to define each axis' grid separately." }, "axisLineWidth": { "default": "0.3", diff --git a/dygraph-options.js b/dygraph-options.js index 55bfe1a..0f90086 100644 --- a/dygraph-options.js +++ b/dygraph-options.js @@ -17,14 +17,10 @@ var DygraphOptions = (function() { "use strict"; /* - * Interesting member variables: - * dygraph_ - the graph. + * Interesting member variables: (REMOVING THIS LIST AS I CLOSURIZE) * global_ - global attributes (common among all graphs, AIUI) * user - attributes set by the user - * yAxes_ - array of axis index to { series : [ series names ] , options : { axis-specific options. } - * xAxis_ - { options : { axis-specific options. } * series_ - { seriesName -> { idx, yAxis, options }} - * labels_ - used as mapping from index to series name. */ /** @@ -34,12 +30,28 @@ var DygraphOptions = (function() { * if labels are not yet available, since those drive details of the per-series * and per-axis options. * - * @param {Dyraph} dygraph The chart to which these options belong. + * @param {Dygraph} dygraph The chart to which these options belong. * @constructor */ var DygraphOptions = function(dygraph) { + /** + * The dygraph. + * @type {!Dygraph} + */ this.dygraph_ = dygraph; + + /** + * Array of axis index to { series : [ series names ] , options : { axis-specific options. } + * @type {Array.<{series : Array., options : Object}>} @private + */ this.yAxes_ = []; + + /** + * Contains x-axis specific options, which are stored in the options key. + * This matches the yAxes_ object structure (by being a dictionary with an + * options element) allowing for shared code. + * @type {options: Object} @private + */ this.xAxis_ = {}; this.series_ = {}; @@ -47,13 +59,22 @@ var DygraphOptions = function(dygraph) { this.global_ = this.dygraph_.attrs_; this.user_ = this.dygraph_.user_attrs_ || {}; + /** + * A list of series in columnar order. + * @type {Array.} + */ + this.labels_ = []; + this.highlightSeries_ = this.get("highlightSeriesOpts") || {}; this.reparseSeries(); }; -/* +/** * Not optimal, but does the trick when you're only using two axes. * If we move to more axes, this can just become a function. + * + * @type {Object.} + * @private */ DygraphOptions.AXIS_STRING_MAPPINGS_ = { 'y' : 0, @@ -64,6 +85,10 @@ DygraphOptions.AXIS_STRING_MAPPINGS_ = { 'Y2' : 1 }; +/** + * @param {string|number} axis + * @private + */ DygraphOptions.axisToIndex_ = function(axis) { if (typeof(axis) == "string") { if (DygraphOptions.AXIS_STRING_MAPPINGS_.hasOwnProperty(axis)) { @@ -77,10 +102,6 @@ DygraphOptions.axisToIndex_ = function(axis) { } throw "Dygraphs only supports two y-axes, indexed from 0-1."; } - if (typeof(axis) == "object") { - throw "Using objects for axis specification " + - "is not supported inside the 'series' option."; - } if (axis) { throw "Unknown axis : " + axis; } @@ -100,7 +121,7 @@ DygraphOptions.prototype.reparseSeries = function() { return; // -- can't do more for now, will parse after getting the labels. } - this.labels = labels.slice(1); + this.labels_ = labels.slice(1); this.yAxes_ = [ { series : [], options : {}} ]; // Always one axis at least. this.xAxis_ = { options : {} }; @@ -133,8 +154,8 @@ DygraphOptions.prototype.reparseSeries = function() { if (oldStyleSeries) { var axisId = 0; // 0-offset; there's always one. // Go through once, add all the series, and for those with {} axis options, add a new axis. - for (var idx = 0; idx < this.labels.length; idx++) { - var seriesName = this.labels[idx]; + for (var idx = 0; idx < this.labels_.length; idx++) { + var seriesName = this.labels_[idx]; var optionsForSeries = this.user_[seriesName] || {}; @@ -155,16 +176,16 @@ DygraphOptions.prototype.reparseSeries = function() { // Go through one more time and assign series to an axis defined by another // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } } - for (var idx = 0; idx < this.labels.length; idx++) { - var seriesName = this.labels[idx]; + for (var idx = 0; idx < this.labels_.length; idx++) { + var seriesName = this.labels_[idx]; var optionsForSeries = this.series_[seriesName]["options"]; var axis = optionsForSeries["axis"]; if (typeof(axis) == 'string') { if (!this.series_.hasOwnProperty(axis)) { - this.dygraph_.error("Series " + seriesName + " wants to share a y-axis with " + + Dygraph.error("Series " + seriesName + " wants to share a y-axis with " + "series " + axis + ", which does not define its own axis."); - return null; + return; } var yAxis = this.series_[axis].yAxis; this.series_[seriesName].yAxis = yAxis; @@ -172,8 +193,8 @@ DygraphOptions.prototype.reparseSeries = function() { } } } else { - for (var idx = 0; idx < this.labels.length; idx++) { - var seriesName = this.labels[idx]; + for (var idx = 0; idx < this.labels_.length; idx++) { + var seriesName = this.labels_[idx]; var optionsForSeries = this.user_.series[seriesName] || {}; var yAxis = DygraphOptions.axisToIndex_(optionsForSeries["axis"]); @@ -293,7 +314,7 @@ DygraphOptions.prototype.getForAxis = function(name, axis) { */ DygraphOptions.prototype.getForSeries = function(name, series) { // Honors indexes as series. - if (series === this.dygraph_.highlightSet_) { + if (series === this.dygraph_.getHighlightSeries()) { if (this.highlightSeries_.hasOwnProperty(name)) { return this.highlightSeries_[name]; } @@ -314,7 +335,7 @@ DygraphOptions.prototype.getForSeries = function(name, series) { /** * Returns the number of y-axes on the chart. - * @return {Number} the number of axes. + * @return {number} the number of axes. */ DygraphOptions.prototype.numAxes = function() { return this.yAxes_.length; diff --git a/dygraph-tickers.js b/dygraph-tickers.js index 4909909..f4778b1 100644 --- a/dygraph-tickers.js +++ b/dygraph-tickers.js @@ -259,6 +259,39 @@ Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6; Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400; Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; +/** + * A collection of objects specifying where it is acceptable to place tick + * marks for granularities larger than WEEKLY. + * 'months' is an array of month indexes on which to place tick marks. + * 'year_mod' ticks are placed when year % year_mod = 0. + * @type {Array.} + */ +Dygraph.LONG_TICK_PLACEMENTS = []; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.MONTHLY] = { + months : [0,1,2,3,4,5,6,7,8,9,10,11], + year_mod : 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.QUARTERLY] = { + months: [0,3,6,9], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.BIANNUAL] = { + months: [0,6], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.ANNUAL] = { + months: [0], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.DECADAL] = { + months: [0], + year_mod: 10 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.CENTENNIAL] = { + months: [0], + year_mod: 100 +}; + /** * This is a list of human-friendly values at which to show tick marks on a log * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so: @@ -312,17 +345,11 @@ Dygraph.numDateTicks = function(start_time, end_time, granularity) { var spacing = Dygraph.SHORT_SPACINGS[granularity]; return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing); } else { - var year_mod = 1; // e.g. to only print one point every 10 years. - var num_months = 12; - if (granularity == Dygraph.QUARTERLY) num_months = 3; - if (granularity == Dygraph.BIANNUAL) num_months = 2; - if (granularity == Dygraph.ANNUAL) num_months = 1; - if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; } - if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; } + var tickPlacement = Dygraph.LONG_TICK_PLACEMENTS[granularity]; var msInYear = 365.2524 * 24 * 3600 * 1000; var num_years = 1.0 * (end_time - start_time) / msInYear; - return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod); + return Math.floor(0.5 + 1.0 * num_years * tickPlacement.months.length / tickPlacement.year_mod); } }; @@ -386,7 +413,7 @@ Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) { var check_dst = (spacing >= Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]); for (t = start_time; t <= end_time; t += spacing) { - var d = new Date(t); + d = new Date(t); // This ensures that we stay on the same hourly "rhythm" across // daylight savings transitions. Without this, the ticks could get off @@ -420,20 +447,9 @@ Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) { var months; var year_mod = 1; // e.g. to only print one point every 10 years. - if (granularity == Dygraph.MONTHLY) { - months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]; - } else if (granularity == Dygraph.QUARTERLY) { - months = [ 0, 3, 6, 9 ]; - } else if (granularity == Dygraph.BIANNUAL) { - months = [ 0, 6 ]; - } else if (granularity == Dygraph.ANNUAL) { - months = [ 0 ]; - } else if (granularity == Dygraph.DECADAL) { - months = [ 0 ]; - year_mod = 10; - } else if (granularity == Dygraph.CENTENNIAL) { - months = [ 0 ]; - year_mod = 100; + if (granularity < Dygraph.NUM_GRANULARITIES) { + months = Dygraph.LONG_TICK_PLACEMENTS[granularity].months; + year_mod = Dygraph.LONG_TICK_PLACEMENTS[granularity].year_mod; } else { Dygraph.warn("Span of dates is too long"); } diff --git a/dygraph-utils.js b/dygraph-utils.js index 5bd58d7..7282c9c 100644 --- a/dygraph-utils.js +++ b/dygraph-utils.js @@ -79,7 +79,7 @@ Dygraph.log = function(severity, message) { // In older versions of Firefox, only console.log is defined. var console = window.console; var log = function(console, method, msg) { - if (method) { + if (method && typeof(method) == 'function') { method.call(console, msg); } else { console.log(msg); @@ -135,7 +135,6 @@ Dygraph.prototype.warn = Dygraph.warn; /** * @param {string} message - * @private */ Dygraph.error = function(message) { Dygraph.log(Dygraph.ERROR, message); @@ -645,7 +644,6 @@ Dygraph.dateStrToMillis = function(str) { * @param {!Object} self * @param {!Object} o * @return {!Object} - * @private */ Dygraph.update = function(self, o) { if (typeof(o) != 'undefined' && o !== null) { @@ -829,7 +827,7 @@ Dygraph.Iterator.prototype.next = function() { }; /** - * Returns a new iterator over array, between indexes start and + * Returns a new iterator over array, between indexes start and * start + length, and only returns entries that pass the accept function * * @param {!Array} array the array to iterate over. @@ -919,7 +917,7 @@ Dygraph.repeatAndCleanup = function(repeatFn, maxFrames, framePeriodInMillis, * This function will scan the option list and determine if they * require us to recalculate the pixel positions of each point. * @param {!Array.} labels a list of options to check. - * @param {!Object} attrs + * @param {!Object} attrs * @return {boolean} true if the graph needs new points else false. * @private */ @@ -1023,7 +1021,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { /** * Compares two arrays to see if they are equal. If either parameter is not an - * array it will return false. Does a shallow compare + * array it will return false. Does a shallow compare * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false. * @param {!Array.} array1 first array * @param {!Array.} array2 second array @@ -1068,7 +1066,7 @@ Dygraph.regularShape_ = function( var computeCoordinates = function() { var x = cx + (Math.sin(angle) * radius); var y = cy + (-Math.cos(angle) * radius); - return [x, y]; + return [x, y]; }; var initialCoordinates = computeCoordinates(); @@ -1176,7 +1174,7 @@ Dygraph.Circles = { * }; * window.addEventListener('mouseup', mouseUpHandler); * }; - * + * * @constructor */ Dygraph.IFrameTarp = function() { @@ -1247,20 +1245,21 @@ Dygraph.detectLineDelimiter = function(data) { }; /** - * Is one element contained by another? - * @param {Element} containee The contained element. - * @param {Element} container The container element. + * Is one node contained by another? + * @param {Node} containee The contained node. + * @param {Node} container The container node. * @return {boolean} Whether containee is inside (or equal to) container. * @private */ -Dygraph.isElementContainedBy = function(containee, container) { +Dygraph.isNodeContainedBy = function(containee, container) { if (container === null || containee === null) { return false; } - while (containee && containee !== container) { - containee = containee.parentNode; + var containeeNode = /** @type {Node} */ (containee); + while (containeeNode && containeeNode !== container) { + containeeNode = containeeNode.parentNode; } - return (containee === container); + return (containeeNode === container); }; diff --git a/dygraph.js b/dygraph.js index dc908e6..4ab7539 100644 --- a/dygraph.js +++ b/dygraph.js @@ -344,18 +344,24 @@ Dygraph.DEFAULT_ATTRS = { pixelsPerLabel: 60, axisLabelFormatter: Dygraph.dateAxisFormatter, valueFormatter: Dygraph.dateString_, + drawGrid: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y2: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: false, + independentTicks: false, ticker: null // will be set in dygraph-tickers.js } } @@ -1016,8 +1022,8 @@ Dygraph.prototype.createInterface_ = function() { // 2. e.relatedTarget is outside the chart var target = e.target || e.fromElement; var relatedTarget = e.relatedTarget || e.toElement; - if (Dygraph.isElementContainedBy(target, dygraph.graphDiv) && - !Dygraph.isElementContainedBy(relatedTarget, dygraph.graphDiv)) { + if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) && + !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { dygraph.mouseOut_(e); } }; @@ -1983,7 +1989,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { ctx.strokeStyle = color; ctx.fillStyle = color; callback(this.g, pt.name, ctx, canvasx, pt.canvasy, - color, circleSize); + color, circleSize, pt.idx); } ctx.restore(); @@ -2282,6 +2288,17 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var datasets = []; var extremes = {}; // series name -> [low, high] var i, j, k; + var errorBars = this.attr_("errorBars"); + var customBars = this.attr_("customBars"); + var bars = errorBars || customBars; + var isValueNull = function(sample) { + if (!bars) { + return sample[1] === null; + } else { + return customBars ? sample[1][1] === null : + errorBars ? sample[1][0] === null : false; + } + }; // Loop over the fields (series). Go from the last to the first, // because if they're stacked that's how we accumulate the values. @@ -2300,11 +2317,11 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { // Prune down to the desired range, if necessary (for zooming) // Because there can be lines going to points outside of the visible area, // we actually prune to visible points, plus one on either side. - var bars = this.attr_("errorBars") || this.attr_("customBars"); if (dateWindow) { var low = dateWindow[0]; var high = dateWindow[1]; var pruned = []; + // TODO(danvk): do binary search instead of linear search. // TODO(danvk): pass firstIdx and lastIdx directly to the renderer. var firstIdx = null, lastIdx = null; @@ -2316,14 +2333,36 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { lastIdx = k; } } + if (firstIdx === null) firstIdx = 0; - if (firstIdx > 0) firstIdx--; + var correctedFirstIdx = firstIdx; + var isInvalidValue = true; + while (isInvalidValue && correctedFirstIdx > 0) { + correctedFirstIdx--; + isInvalidValue = isValueNull(series[correctedFirstIdx]); + } + if (lastIdx === null) lastIdx = series.length - 1; - if (lastIdx < series.length - 1) lastIdx++; - boundaryIds[i-1] = [firstIdx, lastIdx]; + var correctedLastIdx = lastIdx; + isInvalidValue = true; + while (isInvalidValue && correctedLastIdx < series.length - 1) { + correctedLastIdx++; + isInvalidValue = isValueNull(series[correctedLastIdx]); + } + + boundaryIds[i-1] = [(firstIdx > 0) ? firstIdx - 1 : firstIdx, + (lastIdx < series.length - 1) ? lastIdx + 1 : lastIdx]; + + if (correctedFirstIdx!==firstIdx) { + pruned.push(series[correctedFirstIdx]); + } for (k = firstIdx; k <= lastIdx; k++) { pruned.push(series[k]); } + if (correctedLastIdx !== lastIdx) { + pruned.push(series[correctedLastIdx]); + } + series = pruned; } else { boundaryIds[i-1] = [0, series.length-1]; @@ -2592,12 +2631,15 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { }; var numAxes = this.attributes_.numAxes(); var ypadCompat, span, series, ypad; + + var p_axis; // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < numAxes; i++) { var axis = this.axes_[i]; var logscale = this.attributes_.getForAxis("logscale", i); var includeZero = this.attributes_.getForAxis("includeZero", i); + var independentTicks = this.attributes_.getForAxis("independentTicks", i); series = this.attributes_.seriesForAxis(i); // Add some padding. This supports two Y padding operation modes: @@ -2715,20 +2757,33 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { } else { axis.computedValueRange = axis.extremeRange; } - - // Add ticks. By default, all axes inherit the tick positions of the - // primary axis. However, if an axis is specifically marked as having - // independent ticks, then that is permissible as well. - var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); - var ticker = opts('ticker'); - if (i === 0 || axis.independentTicks) { + + + if(independentTicks) { + axis.independentTicks = independentTicks; + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); axis.ticks = ticker(axis.computedValueRange[0], - axis.computedValueRange[1], - this.height_, // TODO(danvk): should be area.height - opts, - this); - } else { - var p_axis = this.axes_[0]; + axis.computedValueRange[1], + this.height_, // TODO(danvk): should be area.height + opts, + this); + // Define the first independent axis as primary axis. + if (!p_axis) p_axis = axis; + } + } + if (p_axis === undefined) { + throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated."); + } + // Add ticks. By default, all axes inherit the tick positions of the + // primary axis. However, if an axis is specifically marked as having + // independent ticks, then that is permissible as well. + for (var i = 0; i < numAxes; i++) { + var axis = this.axes_[i]; + + if (!axis.independentTicks) { + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); var p_ticks = p_axis.ticks; var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0]; var scale = axis.computedValueRange[1] - axis.computedValueRange[0]; @@ -2762,6 +2817,8 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { // TODO(danvk): pre-allocate series here. var series = []; + var errorBars = this.attr_("errorBars"); + var customBars = this.attr_("customBars"); for (var j = 0; j < rawData.length; j++) { var x = rawData[j][0]; var point = rawData[j][i]; @@ -2772,7 +2829,12 @@ Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { point = null; } } - series.push([x, point]); + // Fix null points to fit the display type standard. + if(point !== null) { + series.push([x, point]); + } else { + series.push([x, errorBars ? [null, null] : customBars ? [null, null, null] : point]); + } } return series; }; diff --git a/plugins/axes.js b/plugins/axes.js index f7865c8..cacda86 100644 --- a/plugins/axes.js +++ b/plugins/axes.js @@ -71,9 +71,12 @@ axes.prototype.layout = function(e) { } if (g.numAxes() == 2) { - // TODO(danvk): per-axis setting. - var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize'); - e.reserveSpaceRight(w); + // TODO(danvk): introduce a 'drawAxis' per-axis property. + if (g.getOption('drawYAxis')) { + // TODO(danvk): per-axis setting. + var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize'); + e.reserveSpaceRight(w); + } } else if (g.numAxes() > 2) { g.error("Only two y-axes are supported at this time. (Trying " + "to use " + g.numAxes() + ")"); diff --git a/plugins/grid.js b/plugins/grid.js index 9186dd4..425d93f 100644 --- a/plugins/grid.js +++ b/plugins/grid.js @@ -50,29 +50,56 @@ grid.prototype.willDrawChart = function(e) { var x, y, i, ticks; if (g.getOption('drawYGrid')) { + var axes = ["y", "y2"]; + var strokeStyles = [], lineWidths = [], drawGrid = [], stroking = [], strokePattern = []; + for (var i = 0; i < axes.length; i++) { + drawGrid[i] = g.getOptionForAxis("drawGrid", axes[i]); + if (drawGrid[i]) { + strokeStyles[i] = g.getOptionForAxis('gridLineColor', axes[i]); + lineWidths[i] = g.getOptionForAxis('gridLineWidth', axes[i]); + strokePattern[i] = g.getOptionForAxis('gridLinePattern', axes[i]); + stroking[i] = strokePattern[i] && (strokePattern[i].length >= 2); + } + } ticks = layout.yticks; ctx.save(); - ctx.strokeStyle = g.getOption('gridLineColor'); - ctx.lineWidth = g.getOption('gridLineWidth'); + // draw grids for the different y axes for (i = 0; i < ticks.length; i++) { - // TODO(danvk): allow secondary axes to draw a grid, too. - if (ticks[i][0] !== 0) continue; - x = halfUp(area.x); - y = halfDown(area.y + ticks[i][1] * area.h); - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + area.w, y); - ctx.closePath(); - ctx.stroke(); + var axis = ticks[i][0]; + if(drawGrid[axis]) { + if (stroking[axis]) { + ctx.installPattern(strokePattern[axis]); + } + ctx.strokeStyle = strokeStyles[axis]; + ctx.lineWidth = lineWidths[axis]; + + x = halfUp(area.x); + y = halfDown(area.y + ticks[i][1] * area.h); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + area.w, y); + ctx.closePath(); + ctx.stroke(); + + if (stroking[axis]) { + ctx.uninstallPattern(); + } + } } ctx.restore(); } - if (g.getOption('drawXGrid')) { + // draw grid for x axis + if (g.getOption('drawXGrid') && g.getOptionForAxis("drawGrid", 'x')) { ticks = layout.xticks; ctx.save(); - ctx.strokeStyle = g.getOption('gridLineColor'); - ctx.lineWidth = g.getOption('gridLineWidth'); + var strokePattern = g.getOptionForAxis('gridLinePattern', 'x'); + var stroking = strokePattern && (strokePattern.length >= 2); + if (stroking) { + ctx.installPattern(strokePattern); + } + ctx.strokeStyle = g.getOptionForAxis('gridLineColor', 'x'); + ctx.lineWidth = g.getOptionForAxis('gridLineWidth', 'x'); for (i = 0; i < ticks.length; i++) { x = halfUp(area.x + ticks[i][0] * area.w); y = halfDown(area.y + area.h); @@ -82,6 +109,9 @@ grid.prototype.willDrawChart = function(e) { ctx.closePath(); ctx.stroke(); } + if (stroking) { + ctx.uninstallPattern(); + } ctx.restore(); } }; diff --git a/plugins/range-selector.js b/plugins/range-selector.js index f00a87d..089e64a 100644 --- a/plugins/range-selector.js +++ b/plugins/range-selector.js @@ -176,7 +176,11 @@ rangeSelector.prototype.resize_ = function() { } var plotArea = this.dygraph_.layout_.getPlotArea(); - var xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize')); + + var xAxisLabelHeight = 0; + if(this.getOption_('drawXAxis')){ + xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize')); + } this.canvasRect_ = { x: plotArea.x, y: plotArea.y + plotArea.h + xAxisLabelHeight + 4, diff --git a/test.sh b/test.sh index 3d3bfbb..f54d6e0 100755 --- a/test.sh +++ b/test.sh @@ -9,4 +9,11 @@ if [ $? != 0 ]; then exit 1 fi +# Don't run tests if the documentation doesn't parse. +./generate-documentation.py > /dev/null +if [ $? != 0 ]; then + echo Failed to generate documentation. Fix this before running tests. + exit 1 +fi + phantomjs phantom-driver.js diff --git a/tests/grid_dot.html b/tests/grid_dot.html index 4136ee2..77a271b 100644 --- a/tests/grid_dot.html +++ b/tests/grid_dot.html @@ -31,5 +31,61 @@ } ); + +

Use axis specific grid options. (independentTicks, gridLinePattern, ...)

+
+ + - + \ No newline at end of file diff --git a/tests/independent-series.html b/tests/independent-series.html index eb9a604..bddcc6b 100644 --- a/tests/independent-series.html +++ b/tests/independent-series.html @@ -127,12 +127,12 @@ -
+

The gap would normally be encoded as a null, or missing value. But when you use connectSeparatedPoints, that has a special meaning. Instead, you have to use NaN. This is a bit of a hack, but it gets the job done.

+ + + + + +
+ Index +
 
+0
+1
+2
+3
+4
+5
+6
+7
+8
+
+ (CSV) +
x,A,B
+0,2,1
+1,3,2
+2,3,1
+3,,
+4,4,
+5,3,
+6,3,
+7,3,
+8,4,2
+
+ (native) +
[ 
+  [0, 2, 1], 
+  [1, 3, 2],
+  [2, 3, 1],
+  [3, null, null],
+  [4, 4, null],
+  [5, 3, null],
+  [6, 3, null],
+  [7, 3, null],
+  [8, 4, 2] ]
+
+ diff --git a/tests/missing-data.html b/tests/missing-data.html index fd0bfbd..615a732 100644 --- a/tests/missing-data.html +++ b/tests/missing-data.html @@ -21,6 +21,9 @@
+ +
+
@@ -61,7 +64,7 @@ [2, [12, 2], [20, 3]], [3, [ 8, 2], [20, 3]], [4, [null, 2], [20, 3]], - [5, [null, 2], [null, 3]], + [5, [null, 2], null], [6, [ 9, 2], [20, 3]], [7, [10, 2], [20, 3]] ], @@ -77,8 +80,8 @@ [1, [10, 2], [20, 3]], [2, [12, 2], [20, 3]], [3, [ 8, 2], [20, 3]], - [4, [null, 2], [20, 3]], - [5, [null, 2], [null, 3]], + [4, null, [20, 3]], + [5, null, [null, 3]], [6, [ 9, 2], [20, 3]], [7, [10, 2], [20, 3]] ], @@ -88,6 +91,41 @@ labels: [ "X", "Series1", "Series2" ] } ); + + g5 = new Dygraph( + document.getElementById("graph5"), + [ + [1, [10, 2], [20, 3]], + [2, [12, 2], [20, 3]], + [3, [ 8, 2], [20, 3]], + [4, [2, null], null], + [5, null, [null, 3]], + [6, [ 9, 2], [20, 3]], + [7, [10, 2], [20, 3]] + ], + { + errorBars: true, + connectSeparatedPoints: false, + labels: [ "X", "Series1", "Series2" ] + } + ); + + g6 = new Dygraph( + document.getElementById("graph6"), + [ + [1, [8, 10,12],null], + [2, [3, 5,7],[4,6,7]], + [3, null,[1,2,4]], + [4, [ 9,null, 2],[3,4,8]], + [5, [null,2, null],[6,8,9]], + [6, [2,3, 6],[2,3,5]] + ], + { + customBars: true, + connectSeparatedPoints: false, + labels: [ "X", "Series1", "Series2"] + } + ); diff --git a/tests/range-selector.html b/tests/range-selector.html index 3ad6a26..74a38d6 100644 --- a/tests/range-selector.html +++ b/tests/range-selector.html @@ -29,6 +29,10 @@ Roll period of 14 timesteps, custom range selector height and plot color.

+

+ Demo of range selecor without the chart. (interesting if multiple charts should be synced with one range selector). +

+
diff --git a/tests/two-axes.html b/tests/two-axes.html index 3f40b07..e3e29ff 100644 --- a/tests/two-axes.html +++ b/tests/two-axes.html @@ -16,9 +16,16 @@

Multiple y-axes

The same data with both one and two y-axes. Two y-axes:

+

Two y-axes with y as primary axis (default):

-

A single y-axis:

+

Two y-axes with y2 as primary axis:

+
+

Two y-axes using different grids:

+
+

A single y-axis (left):

+

A single y-axis (right):

+