From 0626a6080e464fdd661a5e80b54a092bc8cac4bb Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 6 Apr 2015 13:49:13 -0700 Subject: [PATCH 1/5] [Plot] Add 2d chart Add skeleton class for implementation of charting functionality which uses canvas' 2D API for rendering, WTD-1070. --- platform/features/plot/src/Canvas2DChart.js | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 platform/features/plot/src/Canvas2DChart.js diff --git a/platform/features/plot/src/Canvas2DChart.js b/platform/features/plot/src/Canvas2DChart.js new file mode 100644 index 0000000000..abcfb58a43 --- /dev/null +++ b/platform/features/plot/src/Canvas2DChart.js @@ -0,0 +1,59 @@ +/*global define,Float32Array*/ + +define( + [], + function () { + "use strict"; + + /** + * Create a new chart which uses Canvas's 2D API for rendering. + * + * @constructor + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if WebGL is unavailable. + */ + function Canvas2DChart(canvas) { + return { + /** + * Clear the chart. + */ + clear: function () { + }, + /** + * Set the logical boundaries of the chart. + * @param {number[]} dimensions the horizontal and + * vertical dimensions of the chart + * @param {number[]} origin the horizontal/vertical + * origin of the chart + */ + setDimensions: function (dimensions, origin) { + }, + /** + * Draw the supplied buffer as a line strip (a sequence + * of line segments), in the chosen color. + * @param {Float32Array} buf the line strip to draw, + * in alternating x/y positions + * @param {number[]} color the color to use when drawing + * the line, as an RGBA color where each element + * is in the range of 0.0-1.0 + * @param {number} points the number of points to draw + */ + drawLine: function (buf, color, points) { + }, + /** + * Draw a rectangle extending from one corner to another, + * in the chosen color. + * @param {number[]} min the first corner of the rectangle + * @param {number[]} max the opposite corner + * @param {number[]} color the color to use when drawing + * the rectangle, as an RGBA color where each element + * is in the range of 0.0-1.0 + */ + drawSquare: function (min, max, color) { + } + }; + } + + return Canvas2DChart; + } +); \ No newline at end of file From eba980c720db055c2eb8a046d984727825422bdc Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 6 Apr 2015 13:56:27 -0700 Subject: [PATCH 2/5] [Plot] Implement canvas 2d methods Implement methods in 2D canvas to support fallback when WebGL is unavailable, WTD-1070. --- platform/features/plot/src/Canvas2DChart.js | 65 ++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/platform/features/plot/src/Canvas2DChart.js b/platform/features/plot/src/Canvas2DChart.js index abcfb58a43..251b6c49d9 100644 --- a/platform/features/plot/src/Canvas2DChart.js +++ b/platform/features/plot/src/Canvas2DChart.js @@ -10,14 +10,46 @@ define( * * @constructor * @param {CanvasElement} canvas the canvas object to render upon - * @throws {Error} an error is thrown if WebGL is unavailable. + * @throws {Error} an error is thrown if Canvas's 2D API is unavailable. */ function Canvas2DChart(canvas) { + var c2d = canvas.getContext('2d'), + dimensions, + origin, + width = canvas.width, + height = canvas.height; + + // Convert from logical to physical x coordinates + function x(v) { + return ((v - origin[0]) / dimensions[0]) * width; + } + + // Convert from logical to physical y coordinates + function y(v) { + return height - ((v - origin[1]) / dimensions[1]) * height; + } + + // Set the color to be used for drawing operations + function setColor(color) { + var mappedColor = color.map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : (c); + }).join(','); + c2d.strokeStyle = "rgba(" + mappedColor + ")"; + c2d.fillStyle = "rgba(" + mappedColor + ")"; + } + + if (!c2d) { + throw new Error("Canvas 2d API unavailable."); + } + return { /** * Clear the chart. */ clear: function () { + width = canvas.width; + height = canvas.height; + c2d.clearRect(0, 0, width, height); }, /** * Set the logical boundaries of the chart. @@ -26,7 +58,9 @@ define( * @param {number[]} origin the horizontal/vertical * origin of the chart */ - setDimensions: function (dimensions, origin) { + setDimensions: function (newDimensions, newOrigin) { + dimensions = newDimensions; + origin = newOrigin; }, /** * Draw the supplied buffer as a line strip (a sequence @@ -39,6 +73,26 @@ define( * @param {number} points the number of points to draw */ drawLine: function (buf, color, points) { + var i; + + setColor(color); + + // Configure context to draw two-pixel-thick lines + c2d.lineWidth = 2; + + // Start a new path... + if (buf.length > 1) { + c2d.beginPath(); + c2d.moveTo(x(buf[0]), y(buf[1])); + } + + // ...and add points to it... + for (i = 2; i < buf.length - 1; i = i + 2) { + c2d.lineTo(x(buf[i]), y(buf[i + 1])); + } + + // ...before finally drawing it. + c2d.stroke(); }, /** * Draw a rectangle extending from one corner to another, @@ -50,6 +104,13 @@ define( * is in the range of 0.0-1.0 */ drawSquare: function (min, max, color) { + var x1 = x(min[0]), + y1 = y(min[1]), + w = x(max[0]) - x1, + h = y(max[1]) - y1; + + setColor(color); + c2d.fillRect(x1, y1, w, h); } }; } From 3db8c1a32d28838370385d7c99c3104e4ad996d7 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 6 Apr 2015 14:15:34 -0700 Subject: [PATCH 3/5] [Plot] Try multiple chart options Choose among multiple chart options, WTD-1070. --- platform/features/plot/src/Canvas2DChart.js | 2 +- platform/features/plot/src/MCTChart.js | 49 ++++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/platform/features/plot/src/Canvas2DChart.js b/platform/features/plot/src/Canvas2DChart.js index 251b6c49d9..244fe75ec3 100644 --- a/platform/features/plot/src/Canvas2DChart.js +++ b/platform/features/plot/src/Canvas2DChart.js @@ -87,7 +87,7 @@ define( } // ...and add points to it... - for (i = 2; i < buf.length - 1; i = i + 2) { + for (i = 2; i < points * 2; i = i + 2) { c2d.lineTo(x(buf[i]), y(buf[i + 1])); } diff --git a/platform/features/plot/src/MCTChart.js b/platform/features/plot/src/MCTChart.js index 43ebaa891f..da044fe39f 100644 --- a/platform/features/plot/src/MCTChart.js +++ b/platform/features/plot/src/MCTChart.js @@ -4,8 +4,8 @@ * Module defining MCTChart. Created by vwoeltje on 11/12/14. */ define( - ["./GLChart"], - function (GLChart) { + ["./GLChart", "./Canvas2DChart"], + function (GLChart, Canvas2DChart) { "use strict"; var TEMPLATE = ""; @@ -43,22 +43,38 @@ define( * @constructor */ function MCTChart($interval, $log) { + // Get an underlying chart implementation + function getChart(Charts, canvas) { + // Try the first available option... + var Chart = Charts[0]; + + // This function recursively try-catches all options; + // if these all fail, issue a warning. + if (!Chart) { + $log.warn("Cannot initialize mct-chart."); + return undefined; + } + + // Try first option; if it fails, try remaining options + try { + return new Chart(canvas); + } catch (e) { + $log.warn([ + "Could not instantiate chart", + Chart.name, + ";", + e.message + ].join(" ")); + + return getChart(Charts.slice(1), canvas); + } + } function linkChart(scope, element) { var canvas = element.find("canvas")[0], activeInterval, 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; - } - // Handle drawing, based on contents of the "draw" object // in scope function doDraw(draw) { @@ -118,6 +134,15 @@ define( } } + // Try to initialize a chart. + chart = getChart([GLChart, Canvas2DChart], canvas); + + // If that failed, there's nothing more we can do here. + // (A warning will already have been issued) + if (!chart) { + return; + } + // Check for resize, on a timer activeInterval = $interval(drawIfResized, 1000); From e01c45df2e0590de5787992b2cf13eb16592230e Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 6 Apr 2015 14:27:43 -0700 Subject: [PATCH 4/5] [Plot] Test 2D chart drawing Test drawing using fallback canvas 2D API, WTD-1070. --- platform/features/plot/src/Canvas2DChart.js | 6 +- .../features/plot/test/Canvas2DChartSpec.js | 72 +++++++++++++++++++ platform/features/plot/test/suite.json | 1 + 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 platform/features/plot/test/Canvas2DChartSpec.js diff --git a/platform/features/plot/src/Canvas2DChart.js b/platform/features/plot/src/Canvas2DChart.js index 244fe75ec3..d3d344d56a 100644 --- a/platform/features/plot/src/Canvas2DChart.js +++ b/platform/features/plot/src/Canvas2DChart.js @@ -14,10 +14,10 @@ define( */ function Canvas2DChart(canvas) { var c2d = canvas.getContext('2d'), - dimensions, - origin, width = canvas.width, - height = canvas.height; + height = canvas.height, + dimensions = [ width, height ], + origin = [ 0, 0 ]; // Convert from logical to physical x coordinates function x(v) { diff --git a/platform/features/plot/test/Canvas2DChartSpec.js b/platform/features/plot/test/Canvas2DChartSpec.js new file mode 100644 index 0000000000..06f53c8bf8 --- /dev/null +++ b/platform/features/plot/test/Canvas2DChartSpec.js @@ -0,0 +1,72 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../src/Canvas2DChart"], + function (Canvas2DChart) { + "use strict"; + + describe("A canvas 2d chart", function () { + var mockCanvas, + mock2d, + chart; + + beforeEach(function () { + mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]); + mock2d = jasmine.createSpyObj( + "2d", + [ + "clearRect", + "beginPath", + "moveTo", + "lineTo", + "stroke", + "fillRect" + ] + ); + mockCanvas.getContext.andReturn(mock2d); + + chart = new Canvas2DChart(mockCanvas); + }); + + it("allows the canvas to be cleared", function () { + chart.clear(); + expect(mock2d.clearRect).toHaveBeenCalled(); + }); + + it("doees not construct if 2D is unavailable", function () { + mockCanvas.getContext.andReturn(undefined); + expect(function () { + return new Canvas2DChart(mockCanvas); + }).toThrow(); + }); + + it("allows dimensions to be set", function () { + // No return value, just verify API is present + chart.setDimensions([120, 120], [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; + chart.drawLine(testBuffer, testColor, testPoints); + expect(mock2d.beginPath).toHaveBeenCalled(); + expect(mock2d.lineTo.calls.length).toEqual(1); + expect(mock2d.stroke).toHaveBeenCalled(); + }); + + it("allows squares to be drawn", function () { + var testMin = [0, 1], + testMax = [10, 10], + testColor = [ 0.25, 0.33, 0.66, 1.0 ]; + + chart.drawSquare(testMin, testMax, testColor); + expect(mock2d.fillRect).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/features/plot/test/suite.json b/platform/features/plot/test/suite.json index 2df3badfef..92ee3b07c8 100644 --- a/platform/features/plot/test/suite.json +++ b/platform/features/plot/test/suite.json @@ -1,4 +1,5 @@ [ + "Canvas2DChart", "GLChart", "MCTChart", "PlotController", From c10253c01d8d9a9c9068c64d6ab6f92088cc6351 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 6 Apr 2015 14:30:13 -0700 Subject: [PATCH 5/5] [Plot] Add note about test priority WTD-1070. --- platform/features/plot/test/Canvas2DChartSpec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/features/plot/test/Canvas2DChartSpec.js b/platform/features/plot/test/Canvas2DChartSpec.js index 06f53c8bf8..4dd1da81ec 100644 --- a/platform/features/plot/test/Canvas2DChartSpec.js +++ b/platform/features/plot/test/Canvas2DChartSpec.js @@ -31,6 +31,10 @@ define( chart = new Canvas2DChart(mockCanvas); }); + // Note that tests below are less specific than they + // could be, esp. w.r.t. arguments to drawing calls; + // this is a fallback option so is a lower test priority. + it("allows the canvas to be cleared", function () { chart.clear(); expect(mock2d.clearRect).toHaveBeenCalled();