diff --git a/platform/features/plot/bundle.json b/platform/features/plot/bundle.json index a4009e8c2c..42766bd3ab 100644 --- a/platform/features/plot/bundle.json +++ b/platform/features/plot/bundle.json @@ -13,7 +13,7 @@ { "key": "mctChart", "implementation": "MCTChart.js", - "depends": [ "$interval" ] + "depends": [ "$interval", "$log" ] } ], "controllers": [ diff --git a/platform/features/plot/src/GLChart.js b/platform/features/plot/src/GLChart.js index 40d9d19049..c7c576f7e3 100644 --- a/platform/features/plot/src/GLChart.js +++ b/platform/features/plot/src/GLChart.js @@ -36,7 +36,7 @@ define( buffer; if (!gl) { - return false; + throw new Error("WebGL unavailable."); } // Initialize shaders diff --git a/platform/features/plot/src/MCTChart.js b/platform/features/plot/src/MCTChart.js index 585f370c74..89d0def6c3 100644 --- a/platform/features/plot/src/MCTChart.js +++ b/platform/features/plot/src/MCTChart.js @@ -14,15 +14,23 @@ define( * * @constructor */ - function MCTChart($interval) { + function MCTChart($interval, $log) { function linkChart(scope, element) { var canvas = element.find("canvas")[0], + chart; + + // Try to initialize GLChart, which allows drawing using WebGL. + // This may fail, particularly where browsers do not support + // WebGL, so catch that here. + try { chart = new GLChart(canvas); + } catch (e) { + $log.warn("Cannot initialize mct-chart; " + e.message); + return; + } - function doDraw() { - var draw = scope.draw; - + function doDraw(draw) { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; chart.clear(); @@ -57,7 +65,7 @@ define( function drawIfResized() { if (canvas.width !== canvas.offsetWidth || canvas.height !== canvas.offsetHeight) { - doDraw(); + doDraw(scope.draw); } } diff --git a/platform/features/plot/test/GLChartSpec.js b/platform/features/plot/test/GLChartSpec.js index 267794aea6..cfd38d0638 100644 --- a/platform/features/plot/test/GLChartSpec.js +++ b/platform/features/plot/test/GLChartSpec.js @@ -9,6 +9,103 @@ define( "use strict"; describe("A WebGL chart", function () { + var mockCanvas, + mockGL, + glChart; + + beforeEach(function () { + mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]); + mockGL = jasmine.createSpyObj( + "gl", + [ + "createShader", + "compileShader", + "shaderSource", + "attachShader", + "createProgram", + "linkProgram", + "useProgram", + "enableVertexAttribArray", + "getAttribLocation", + "getUniformLocation", + "createBuffer", + "lineWidth", + "enable", + "blendFunc", + "viewport", + "clear", + "uniform2fv", + "uniform4fv", + "bufferData", + "bindBuffer", + "vertexAttribPointer", + "drawArrays" + ] + ); + mockGL.ARRAY_BUFFER = "ARRAY_BUFFER"; + mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW"; + mockGL.TRIANGLE_FAN = "TRIANGLE_FAN"; + mockGL.LINE_STRIP = "LINE_STRIP"; + + // Echo back names for uniform locations, so we can + // test which of these are set for certain operations. + mockGL.getUniformLocation.andCallFake(function (a, name) { + return name; + }); + + mockCanvas.getContext.andReturn(mockGL); + + glChart = new GLChart(mockCanvas); + }); + + it("allows the canvas to be cleared", function () { + glChart.clear(); + expect(mockGL.clear).toHaveBeenCalled(); + }); + + it("doees not construct if WebGL is unavailable", function () { + mockCanvas.getContext.andReturn(undefined); + expect(function () { + return new GLChart(mockCanvas); + }).toThrow(); + }); + + it("allows dimensions to be set", function () { + glChart.setDimensions([120, 120], [0, 10]); + expect(mockGL.uniform2fv) + .toHaveBeenCalledWith("uDimensions", [120, 120]); + expect(mockGL.uniform2fv) + .toHaveBeenCalledWith("uOrigin", [0, 10]); + }); + + it("allows lines to be drawn", function () { + var testBuffer = [ 0, 1, 3, 8 ], + testColor = [ 0.25, 0.33, 0.66, 1.0 ], + testPoints = 2; + glChart.drawLine(testBuffer, testColor, testPoints); + expect(mockGL.bufferData).toHaveBeenCalledWith( + mockGL.ARRAY_BUFFER, + testBuffer, + mockGL.DYNAMIC_DRAW + ); + expect(mockGL.uniform4fv) + .toHaveBeenCalledWith("uColor", testColor); + expect(mockGL.drawArrays) + .toHaveBeenCalledWith("LINE_STRIP", 0, testPoints); + }); + + it("allows squares to be drawn", function () { + var testMin = [0, 1], + testMax = [10, 10], + testColor = [ 0.25, 0.33, 0.66, 1.0 ]; + + glChart.drawSquare(testMin, testMax, testColor); + + expect(mockGL.uniform4fv) + .toHaveBeenCalledWith("uColor", testColor); + expect(mockGL.drawArrays) + .toHaveBeenCalledWith("TRIANGLE_FAN", 0, 4); + }); }); } ); \ No newline at end of file diff --git a/platform/features/plot/test/MCTChartSpec.js b/platform/features/plot/test/MCTChartSpec.js index d298691d28..84d6a27368 100644 --- a/platform/features/plot/test/MCTChartSpec.js +++ b/platform/features/plot/test/MCTChartSpec.js @@ -9,6 +9,147 @@ define( "use strict"; describe("The mct-chart directive", function () { + var mockInterval, + mockLog, + mockScope, + mockElement, + mockCanvas, + mockGL, + mctChart; + + beforeEach(function () { + mockInterval = + jasmine.createSpy("$interval"); + mockLog = + jasmine.createSpyObj("$log", ["warn", "info", "debug"]); + mockScope = + jasmine.createSpyObj("$scope", ["$watchCollection"]); + mockElement = + jasmine.createSpyObj("element", ["find"]); + + + // mct-chart uses GLChart, so it needs WebGL API + mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]); + mockGL = jasmine.createSpyObj( + "gl", + [ + "createShader", + "compileShader", + "shaderSource", + "attachShader", + "createProgram", + "linkProgram", + "useProgram", + "enableVertexAttribArray", + "getAttribLocation", + "getUniformLocation", + "createBuffer", + "lineWidth", + "enable", + "blendFunc", + "viewport", + "clear", + "uniform2fv", + "uniform4fv", + "bufferData", + "bindBuffer", + "vertexAttribPointer", + "drawArrays" + ] + ); + mockGL.ARRAY_BUFFER = "ARRAY_BUFFER"; + mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW"; + mockGL.TRIANGLE_FAN = "TRIANGLE_FAN"; + mockGL.LINE_STRIP = "LINE_STRIP"; + + // Echo back names for uniform locations, so we can + // test which of these are set for certain operations. + mockGL.getUniformLocation.andCallFake(function (a, name) { + return name; + }); + + mockElement.find.andReturn([mockCanvas]); + mockCanvas.getContext.andReturn(mockGL); + + mctChart = new MCTChart(mockInterval, mockLog); + }); + + it("is applicable at the element level", function () { + expect(mctChart.restrict).toEqual("E"); + }); + + it("places a 'draw' attribute in-scope", function () { + // Should ask Angular for the draw attribute + expect(mctChart.scope.draw).toEqual("="); + }); + + it("watches for changes in the drawn object", function () { + mctChart.link(mockScope, mockElement); + expect(mockScope.$watchCollection) + .toHaveBeenCalledWith("draw", jasmine.any(Function)); + }); + + it("issues one draw call per line", function () { + mctChart.link(mockScope, mockElement); + mockScope.$watchCollection.mostRecentCall.args[1]({ + lines: [ {}, {}, {} ] + }); + expect(mockGL.drawArrays.calls.length).toEqual(3); + }); + + it("issues one draw call per box", function () { + mctChart.link(mockScope, mockElement); + mockScope.$watchCollection.mostRecentCall.args[1]({ + boxes: [ + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] } + ] + }); + expect(mockGL.drawArrays.calls.length).toEqual(4); + }); + + it("does not fail if no draw object is in scope", function () { + mctChart.link(mockScope, mockElement); + expect(mockScope.$watchCollection.mostRecentCall.args[1]) + .not.toThrow(); + }); + + it("draws on canvas resize", function () { + mctChart.link(mockScope, mockElement); + + // Should track canvas size in an interval + expect(mockInterval).toHaveBeenCalledWith( + jasmine.any(Function), + jasmine.any(Number) + ); + + // Verify pre-condition + expect(mockGL.clear).not.toHaveBeenCalled(); + + mockCanvas.width = 100; + mockCanvas.offsetWidth = 150; + mockCanvas.height = 200; + mockCanvas.offsetHeight = 200; + mockInterval.mostRecentCall.args[0](); + + // Use clear as an indication that drawing has occurred + expect(mockGL.clear).toHaveBeenCalled(); + }); + + it("warns if no WebGL context is available", function () { + mockCanvas.getContext.andReturn(undefined); + mctChart.link(mockScope, mockElement); + expect(mockLog.warn).toHaveBeenCalled(); + }); + + it("logs nothing in nominal situations (WebGL available)", function () { + // Complement the previous test + mctChart.link(mockScope, mockElement); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + }); } ); \ No newline at end of file