From b40494ac95cb2927501e08c3e7a728696695f1dc Mon Sep 17 00:00:00 2001 From: larkin Date: Fri, 12 Jun 2015 13:53:53 -0700 Subject: [PATCH 1/9] plot-reborn WIP --- bundles.json | 2 +- platform/features/plot-reborn/README.md | 57 ++++ platform/features/plot-reborn/bundle.json | 89 ++++++ .../plot-reborn/res/templates/mct-plot.html | 76 ++++++ .../plot-reborn/res/templates/plot.html | 9 + .../res/templates/stacked-plot.html | 8 + .../src/controllers/PlotController.js | 177 ++++++++++++ .../src/controllers/StackedPlotController.js | 37 +++ .../plot-reborn/src/directives/MCTChart.js | 173 ++++++++++++ .../src/directives/MCTOverlayPlot.js | 14 + .../plot-reborn/src/directives/MCTPlot.js | 254 ++++++++++++++++++ .../features/plot-reborn/src/draw/Draw2D.js | 120 +++++++++ .../plot-reborn/src/draw/DrawLoader.js | 45 ++++ .../plot-reborn/src/draw/DrawWebGL.js | 152 +++++++++++ .../features/plot-reborn/src/lib/utils.js | 75 ++++++ 15 files changed, 1287 insertions(+), 1 deletion(-) create mode 100644 platform/features/plot-reborn/README.md create mode 100644 platform/features/plot-reborn/bundle.json create mode 100644 platform/features/plot-reborn/res/templates/mct-plot.html create mode 100644 platform/features/plot-reborn/res/templates/plot.html create mode 100644 platform/features/plot-reborn/res/templates/stacked-plot.html create mode 100644 platform/features/plot-reborn/src/controllers/PlotController.js create mode 100644 platform/features/plot-reborn/src/controllers/StackedPlotController.js create mode 100644 platform/features/plot-reborn/src/directives/MCTChart.js create mode 100644 platform/features/plot-reborn/src/directives/MCTOverlayPlot.js create mode 100644 platform/features/plot-reborn/src/directives/MCTPlot.js create mode 100644 platform/features/plot-reborn/src/draw/Draw2D.js create mode 100644 platform/features/plot-reborn/src/draw/DrawLoader.js create mode 100644 platform/features/plot-reborn/src/draw/DrawWebGL.js create mode 100644 platform/features/plot-reborn/src/lib/utils.js diff --git a/bundles.json b/bundles.json index de005c5961..064afce234 100644 --- a/bundles.json +++ b/bundles.json @@ -11,7 +11,7 @@ "platform/telemetry", "platform/features/layout", "platform/features/pages", - "platform/features/plot", + "platform/features/plot-reborn", "platform/features/scrolling", "platform/forms", "platform/persistence/queue", diff --git a/platform/features/plot-reborn/README.md b/platform/features/plot-reborn/README.md new file mode 100644 index 0000000000..de01ec4e2d --- /dev/null +++ b/platform/features/plot-reborn/README.md @@ -0,0 +1,57 @@ +# plot-reborn + +The `plot-reborn` bundle provides directives for composing plot based views. +It also exposes domain objects for plotting telemetry points. + +## Views + +* OverlayPlot: can be used on any domain object that has or delegates a + telemetry capability. + +* StackedPlot: can be used on any domain object that delegates telemetry or + delegates composition of elements that have telemetry. + +## Directives + +* `mct-chart`: an element that takes `series`, `viewport`, and + `rectangles` and plots the data. Adding points to a series after it has + been initially plotted can be done either by recreating the series object + or by broadcasting "series:data:add" with arguments `event`, `seriesIndex`, + `points`. This will append `points` to the `series` at index `seriesIndex`. + +* `mct-plot`: A directive that wraps a mct-chart and handles user interactions + with that plot. It emits events that a parent view can use for coordinating + functionality: + * emits a `user:viewport:change:start` event when the viewport begins being + changed by a user, to allow any parent controller to prevent viewport + modifications while the user is interacting with the plot. + * emits a `user:viewport:change:end` event when the user has finished + changing the viewport. This allows a controller on a parent scope to + track viewport history and provide any necessary functionality + around viewport changes, e.g. viewport history. + +* `mct-overlay-plot`: A directive that takes `domainObject` and plots either a + single series of data (in the case of a single telemetry object) or multiple + series of data (in the case of a object which delegates telemetry). + +## Controllers + +NOTE: this section not accurate. Essentially, these controllers format data for +the mct-chart directive. They also handle live viewport updating, as well as +managing all transformations from domain objects to views. + +* StackPlotController: Uses the composition capability of a StackPlot domain + object to retrieve SubPlots and render them with individual PlotControllers. +* PlotController: Uses either a domain object that delegates telemetry or a + domain object with telemetry to and feeds that data to the mct-chart + directive. + +## TODOS: + +* [ ] Re-implement history stack. +* [ ] Re-implement plot pallette. +* [ ] Re-implement stacked plot viewport synchronization (share viewport object) +* [ ] Other things? +* [ ] Handle edge cases with marquee zoom/panning. +* [ ] Tidy code. + diff --git a/platform/features/plot-reborn/bundle.json b/platform/features/plot-reborn/bundle.json new file mode 100644 index 0000000000..b526ea0323 --- /dev/null +++ b/platform/features/plot-reborn/bundle.json @@ -0,0 +1,89 @@ +{ + "name": "Plot view for telemetry, reborn", + "extensions": { + "views": [ + { + "name": "Plot", + "key": "plot-single", + "glyph": "6", + "templateUrl": "templates/plot.html", + "needs": ["telemetry"], + "uses": ["composition"], + "delegation": false + }, + { + "name": "Overlay Plot", + "key": "plot", + "glyph": "6", + "templateUrl": "templates/plot.html", + "needs": ["telemetry", "composition"], + "uses": ["composition"], + "delegation": true + }, + { + "name": "Stacked Plot", + "key": "stackedPlot", + "glyph": "6", + "templateUrl": "templates/stacked-plot.html", + "needs": ["composition", "delegation"], + "uses": ["composition"], + "gestures": [ "drop" ], + "delegation": true + } + ], + "directives": [ + { + "key": "mctChart", + "implementation": "directives/MCTChart.js", + "depends": [ "$interval", "$log" ] + }, + { + "key": "mctPlot", + "implementation": "directives/MCTPlot.js", + "depends": [], + "templateUrl": "templates/mct-plot.html" + }, + { + "key": "mctOverlayPlot", + "implementation": "directives/MCTOverlayPlot.js", + "depends": [] + } + ], + "controllers": [ + { + "key": "PlotController", + "implementation": "controllers/PlotController.js", + "depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ] + }, + { + "key": "StackedPlotController", + "implementation": "controllers/StackedPlotController.js", + "depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ] + } + ], + "types": [ + { + "key": "telemetry.plot.overlay", + "name": "Overlay Plot", + "glyph": "t", + "description": "A plot containing one or more telemetry elements.", + "delegates": ["telemetry"], + "features": "creation", + "contains": [{"has": "telemetry"}], + "model": {"composition": []}, + "properties": [] + }, + { + "key": "telemetry.plot.stacked", + "name": "Stacked Plot", + "glyph": "t", + "description": "A stacked plot of overlay plots.", + "delegates": ["delegation"], + "features": "creation", + "contains": ["telemetry.plot.overlay", {"has": "telemetry"}], + "model": {"composition": []}, + "properties": [] + } + ] + } +} diff --git a/platform/features/plot-reborn/res/templates/mct-plot.html b/platform/features/plot-reborn/res/templates/mct-plot.html new file mode 100644 index 0000000000..c3b3096f83 --- /dev/null +++ b/platform/features/plot-reborn/res/templates/mct-plot.html @@ -0,0 +1,76 @@ + +
+
+ + + + {{ series.name }} + +
+ +
+ {{ displayableDomain(mouseCoordinates.positionAsPlotPoint.domain) }}, + {{ displayableRange(mouseCoordinates.positionAsPlotPoint.range) }} + +
+ +
+ +
+ {{ axes.range.name}} +
+ +
+ {{ displayableRange(tick) }} +
+
+ +
+ +
+ +
+ +
+ +
+ + + + + + + +
+ +
+
+ {{ displayableDomain(tick) }} +
+ +
+ {{ axes.domain.name }} +
+
+
diff --git a/platform/features/plot-reborn/res/templates/plot.html b/platform/features/plot-reborn/res/templates/plot.html new file mode 100644 index 0000000000..5656adee31 --- /dev/null +++ b/platform/features/plot-reborn/res/templates/plot.html @@ -0,0 +1,9 @@ + + + + diff --git a/platform/features/plot-reborn/res/templates/stacked-plot.html b/platform/features/plot-reborn/res/templates/stacked-plot.html new file mode 100644 index 0000000000..20b50f3485 --- /dev/null +++ b/platform/features/plot-reborn/res/templates/stacked-plot.html @@ -0,0 +1,8 @@ + +
+ + +
+
diff --git a/platform/features/plot-reborn/src/controllers/PlotController.js b/platform/features/plot-reborn/src/controllers/PlotController.js new file mode 100644 index 0000000000..4889ea004a --- /dev/null +++ b/platform/features/plot-reborn/src/controllers/PlotController.js @@ -0,0 +1,177 @@ +/*global define*/ + +define( + function () { + "use strict"; + + // TODO: Store this in more accessible locations / retrieve from + // domainObject metadata. + var DOMAIN_INTERVAL = 1 * 60 * 1000; // One minute. + + function PlotController($scope) { + var plotHistory = []; + var isLive = true; + var maxDomain = 0; + var domainOffset = +new Date(); + var subscriptions = []; + var setToDefaultViewport = function() { + $scope.viewport = { + topLeft: { + domain: maxDomain - DOMAIN_INTERVAL, + range: 1 + }, + bottomRight: { + domain: maxDomain, + range: -1 + } + }; + }; + + setToDefaultViewport(); + + $scope.displayableRange = function(rangeValue) { + // TODO: Call format function provided by domain object. + return rangeValue; + }; + $scope.displayableDomain = function(domainValue) { + // TODO: Call format function provided by domain object. + return new Date(domainValue + domainOffset).toUTCString(); + }; + + $scope.series = []; + + $scope.rectangles = []; + + var updateSeriesFromTelemetry = function(series, seriesIndex, telemetry) { + if (typeof domainOffset === 'undefined') { + domainOffset = telemetry.getDomainValue(telemetry.getPointCount() - 1); + } + var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1) - domainOffset; + var rangeValue = telemetry.getRangeValue(telemetry.getPointCount() - 1); + // Track the biggest domain we've seen for sticky-ness. + maxDomain = Math.max(maxDomain, domainValue); + + var newTelemetry = { + domain: domainValue, + range: rangeValue + }; + series.data.push(newTelemetry); + $scope.$broadcast('series:data:add', seriesIndex, [newTelemetry]); + }; + + var subscribeToDomainObject = function(domainObject) { + var telemetryCapability = domainObject.getCapability('telemetry'); + var model = domainObject.getModel(); + + var series = { + name: model.name, + // TODO: Bring back PlotPalette. + color: [0.12549019607843137, 0.6980392156862745, 0.6666666666666666, 1], + data: [] + }; + + $scope.series.push(series); + var seriesIndex = $scope.series.indexOf(series); + + var updater = updateSeriesFromTelemetry.bind( + null, + series, + seriesIndex + ); + subscriptions.push(telemetryCapability.subscribe(updater)); + }; + + var linkDomainObject = function(domainObject) { + unlinkDomainObject(); + if (domainObject.hasCapability('telemetry')) { + subscribeToDomainObject(domainObject); + } else if (domainObject.hasCapability('delegation')) { + // Makes no sense that we have to use a subscription to get domain objects associated with delegates (and their names). We can map the same series generation code to telemetry delegates; Let's do that ourselves. + var subscribeToDelegates = function(delegates) { + return delegates.forEach(subscribeToDomainObject); + // TODO: Should return a promise. + }; + domainObject + .getCapability('delegation') + .getDelegates('telemetry') + .then(subscribeToDelegates); + // TODO: should have a catch. + } else { + throw new Error('Domain object type not supported.'); + } + }; + + var unlinkDomainObject = function() { + subscriptions.forEach(function(subscription) { + subscription.unsubscribe(); + }); + subscriptions = []; + }; + + var onUserViewportChangeStart = function() { + // TODO: this is a great time to track a history entry. + // Disable live mode so they have full control of viewport. + plotHistory.push($scope.viewport); + isLive = false; + }; + + var onUserViewportChangeEnd = function(event, viewport) { + // If the new viewport is "close enough" to the maxDomain then + // enable live mode. Set empirically to 10% of the domain + // interval. + // TODO: Better UX pattern for this. + + if (Math.abs(maxDomain - viewport.bottomRight.domain) < (DOMAIN_INTERVAL/10)) { + isLive = true; + $scope.viewport.bottomRight.domain = maxDomain; + } else { + isLive = false; + } + plotHistory.push(viewport); + }; + + var viewportForMaxDomain = function() { + var viewport = { + topLeft: { + range: $scope.viewport.topLeft.range, + domain: maxDomain - DOMAIN_INTERVAL + }, + bottomRight: { + range: $scope.viewport.bottomRight.range, + domain: maxDomain + } + }; + return viewport; + }; + + var followDataIfLive = function() { + if (isLive) { + $scope.viewport = viewportForMaxDomain(); + } + }; + + $scope.$on('series:data:add', followDataIfLive); + $scope.$on('user:viewport:change:end', onUserViewportChangeEnd); + $scope.$on('user:viewport:change:start', onUserViewportChangeStart); + + $scope.$watch('domainObject', linkDomainObject); + + var controller = { + historyBack: function() { + // TODO: Step History Back. + }, + historyForward: function() { + // TODO: Step History Forward. + }, + resetZoom: function() { + // TODO: Reset view to defaults. Keep history stack alive? + } + }; + + return controller; + } + + return PlotController; + + } +); diff --git a/platform/features/plot-reborn/src/controllers/StackedPlotController.js b/platform/features/plot-reborn/src/controllers/StackedPlotController.js new file mode 100644 index 0000000000..af787656c8 --- /dev/null +++ b/platform/features/plot-reborn/src/controllers/StackedPlotController.js @@ -0,0 +1,37 @@ +/*global define */ + +define( + function () { + "use strict"; + + function StackedPlotController($scope) { + + $scope.telemetryObjects = []; + + var linkDomainObject = function(domainObject) { + if (domainObject.hasCapability('telemetry')) { + $scope.telemetryObjects = [domainObject]; + } else if (domainObject.hasCapability('delegation')) { + + var addObjectsIfCompatible = function(objects) { + objects.forEach(function(object) { + if (object.hasCapability('telemetry')) { + $scope.telemetryObjects.push(object); + } else if (object.hasCapability('delegation')) { + $scope.telemetryObjects.push(object); + } + }); + }; + domainObject + .useCapability('composition') + .then(addObjectsIfCompatible); + // TODO: should have a catch. + } else { + throw new Error('Domain object type not supported.'); + } + }; + $scope.$watch('domainObject', linkDomainObject); + } + return StackedPlotController; + } +); diff --git a/platform/features/plot-reborn/src/directives/MCTChart.js b/platform/features/plot-reborn/src/directives/MCTChart.js new file mode 100644 index 0000000000..7b88a43243 --- /dev/null +++ b/platform/features/plot-reborn/src/directives/MCTChart.js @@ -0,0 +1,173 @@ +/*global define,requestAnimationFrame,Float32Array*/ + +/** + * Module defining MCTChart. Created by vwoeltje on 11/12/14. + */ +define( + ["../draw/DrawLoader"], + function (DrawLoader) { + "use strict"; + + var TEMPLATE = ""; + + /** + * MCTChart draws charts utilizing a drawAPI. + * + * @constructor + */ + function MCTChart($interval) { + + function linkChart($scope, $element) { + var canvas = $element.find("canvas")[0], + isDestroyed = false, + activeInterval, + drawAPI, + lines = []; + + drawAPI = DrawLoader.getDrawAPI(canvas); + + if (!drawAPI) { + return; + } + + function redraw() { + if (isDestroyed) { + return; + } + requestAnimationFrame(redraw); + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + drawAPI.clear(); + updateViewport(); + drawSeries(); + drawRectangles(); + } + + function drawIfResized() { + if (canvas.width !== canvas.offsetWidth || + canvas.height !== canvas.offsetHeight) { + redraw(); + } + } + + function destroyChart() { + isDestroyed = true; + if (activeInterval) { + $interval.cancel(activeInterval); + } + } + + function drawSeries() { + // TODO: Don't regenerate lines on each frame. + lines = $scope.series.map(lineFromSeries); + lines.forEach(function(line) { + drawAPI.drawLine( + line.buffer, + line.color, + line.pointCount + ); + }); + } + + function drawRectangles() { + if ($scope.rectangles) { + $scope.rectangles.forEach(function(rect) { + drawAPI.drawSquare( + [rect.start.domain, rect.start.range], + [rect.end.domain, rect.end.range], + rect.color + ); + }); + } + } + + function updateViewport() { + var dimensions = [ + Math.abs($scope.viewport.topLeft.domain - $scope.viewport.bottomRight.domain), + Math.abs($scope.viewport.topLeft.range - $scope.viewport.bottomRight.range) + ]; + + var origin = [ + $scope.viewport.topLeft.domain, + $scope.viewport.bottomRight.range + ]; + + drawAPI.setDimensions( + dimensions, + origin + ); + } + + function lineFromSeries(series) { + // TODO: handle when lines get longer than 10,000 points. + // Each line allocates 10,000 points. This should be more + // that we ever need, but we have to decide how to handle + // this at the higher level. I imagine the plot controller + // should watch it's series and when they get huge, slice + // them in half and delete the oldest half. + // + // As long as the controller replaces $scope.series with a + // new series object, then this directive will + // automatically generate new arrays for those lines. + // In practice, the overhead of regenerating these lines + // appears minimal. + var lineBuffer = new Float32Array(20000); + for (var i = 0; i < series.data.length; i++) { + lineBuffer[2*i] = series.data[i].domain; + lineBuffer[2*i+1] = series.data[i].range; + } + return { + color: series.color, + buffer: lineBuffer, + pointCount: series.data.length + }; + } + + function initializeLines() { + lines = $scope.series.map(lineFromSeries); + } + + function onSeriesDataAdd(event, seriesIndex, points) { + var line = lines[seriesIndex]; + points.forEach(function (point) { + line.buffer[2*line.pointCount] = point.domain; + line.buffer[2*line.pointCount+1] = point.range; + line.pointCount += 1; + }); + } + + // Check for resize, on a timer + activeInterval = $interval(drawIfResized, 1000); + + // Initialize series + $scope.$watch('series', initializeLines); + $scope.$on('series:data:add', onSeriesDataAdd); + redraw(); + + // Stop checking for resize when $scope is destroyed + $scope.$on("$destroy", destroyChart); + } + + return { + // Apply directive only to $elements + restrict: "E", + + // Template to use (a canvas $element) + template: TEMPLATE, + + // Link function; set up $scope + link: linkChart, + + // Initial, isolate $scope for the directive + scope: { + draw: "=" , + rectangles: "=", + series: "=", + viewport: "=" + } + }; + } + + return MCTChart; + } +); diff --git a/platform/features/plot-reborn/src/directives/MCTOverlayPlot.js b/platform/features/plot-reborn/src/directives/MCTOverlayPlot.js new file mode 100644 index 0000000000..a0b74dabd1 --- /dev/null +++ b/platform/features/plot-reborn/src/directives/MCTOverlayPlot.js @@ -0,0 +1,14 @@ +/*global define*/ + +define(function () { + return function MCTOverlayPlot() { + return { + restrict: "E", + templateUrl: 'platform/features/plot-reborn/res/templates/plot.html', + scope: { + domainObject: "=" + } + }; + }; + } +); diff --git a/platform/features/plot-reborn/src/directives/MCTPlot.js b/platform/features/plot-reborn/src/directives/MCTPlot.js new file mode 100644 index 0000000000..20832ce1d3 --- /dev/null +++ b/platform/features/plot-reborn/src/directives/MCTPlot.js @@ -0,0 +1,254 @@ +/*global define,window*/ + +define( + [ + '../lib/utils' + ], + function (utils) { + "use strict"; + + var RANGE_TICK_COUNT = 7; + var DOMAIN_TICK_COUNT = 5; + + function MCTPlot() { + + function link($scope, $element) { + // Now that we're here, let's handle some scope management that the controller would otherwise handle. + $scope.axes = { + domain: { + label: "Time", + tickCount: DOMAIN_TICK_COUNT, + ticks: [] + }, + range: { + label: "Value", + tickCount: RANGE_TICK_COUNT, + ticks: [] + } + }; + + var dragStart; + var marqueeBox = {}; + var marqueeRect; // Set when exists. + var chartElementBounds; + var $canvas = $element.find('canvas'); + + var updateAxesForCurrentViewport = function() { + // Update axes definitions for current viewport. + ['domain', 'range'].forEach(function(axisName) { + var axis = $scope.axes[axisName]; + var firstTick = $scope.viewport.topLeft[axisName]; + var lastTick = $scope.viewport.bottomRight[axisName]; + var axisSize = firstTick - lastTick; + var denominator = axis.tickCount - 1; + // Yes, ticksize is negative for domain and positive for range. + // It's because ticks are generated/displayed top to bottom and left to right. + axis.ticks = []; + for (var tickNumber = 0; tickNumber < axis.tickCount; tickNumber++) { + var tickIncrement = (axisSize * (tickNumber / denominator)); + var tickValue = firstTick - tickIncrement; + axis.ticks.push( + tickValue + ); + } + }); + }; + + var drawMarquee = function() { + // Create rectangle for Marquee if it should be set. + if (marqueeBox && marqueeBox.start && marqueeBox.end) { + if (!marqueeRect) { + marqueeRect = {}; + $scope.rectangles.push(marqueeRect); + } + marqueeRect.start = marqueeBox.start; + marqueeRect.end = marqueeBox.end; + marqueeRect.color = [1, 1, 1, 0.5]; + marqueeRect.layer = 'top'; // TODO: implement this. + $scope.$broadcast('rectangle-change'); + } else if (marqueeRect && $scope.rectangles.indexOf(marqueeRect) != -1) { + $scope.rectangles.splice($scope.rectangles.indexOf(marqueeRect)); + marqueeRect = undefined; + $scope.$broadcast('rectangle-change'); + } + }; + + var untrackMousePosition = function() { + $scope.mouseCoordinates = undefined; + }; + + var trackMousePosition = function($event) { + // Calculate coordinates of mouse related to canvas and as + // domain, range value and make available in scope for display. + + var bounds = $event.target.getBoundingClientRect(); + chartElementBounds = bounds; + + var positionOverElement = { + x: $event.clientX - bounds.left, + y: $event.clientY - bounds.top + }; + + var positionAsPlotPoint = utils.elementPositionAsPlotPosition( + positionOverElement, + bounds, + $scope.viewport + ); + + $scope.mouseCoordinates = { + positionOverElement: positionOverElement, + positionAsPlotPoint: positionAsPlotPoint + }; + + if (marqueeBox && marqueeBox.start) { + updateMarquee(); + } + + if (dragStart) { + updateDrag(); + } + }; + + var startMarquee = function() { + marqueeBox.start = $scope.mouseCoordinates.positionAsPlotPoint; + }; + + var updateMarquee = function() { + // Update the marquee box in progress. + marqueeBox.end = $scope.mouseCoordinates.positionAsPlotPoint; + drawMarquee(); + }; + + var endMarquee = function() { + // marqueeBox start/end are opposite corners but we need + // topLeft and bottomRight. + var boxPoints = utils.boxPointsFromOppositeCorners(marqueeBox.start, marqueeBox.end); + var newViewport = utils.oppositeCornersFromBoxPoints(boxPoints); + + marqueeBox = {}; + drawMarquee(); + $scope.$emit('user:viewport:change:end', newViewport); + $scope.viewport = newViewport; + }; + + var startDrag = function($event) { + $scope.$emit('user:viewport:change:start'); + if (!$scope.mouseCoordinates) { + return; + } + $event.preventDefault(); + // Track drag location relative to position over element + // not domain, as chart viewport will change as we drag. + dragStart = $scope.mouseCoordinates.positionAsPlotPoint; + // Tell controller that we're starting to navigate. + return false; + }; + + var updateDrag = function() { + // calculate offset between points. Apply that offset to viewport. + var newPosition = $scope.mouseCoordinates.positionAsPlotPoint; + var dDomain = dragStart.domain - newPosition.domain; + var dRange = dragStart.range - newPosition.range; + + $scope.viewport = { + topLeft: { + domain: $scope.viewport.topLeft.domain + dDomain, + range: $scope.viewport.topLeft.range + dRange + }, + bottomRight: { + domain: $scope.viewport.bottomRight.domain + dDomain, + range: $scope.viewport.bottomRight.range + dRange + } + }; + }; + + var endDrag = function() { + dragStart = undefined; + $scope.$emit('user:viewport:change:end', $scope.viewport); + }; + + var watchForMarquee = function() { + $canvas.removeClass('plot-drag'); + $canvas.addClass('plot-marquee'); + $canvas.on('mousedown', startMarquee); + $canvas.on('mouseup', endMarquee); + $canvas.off('mousedown', startDrag); + $canvas.off('mouseup', endDrag); + }; + + var watchForDrag = function() { + $canvas.addClass('plot-drag'); + $canvas.removeClass('plot-marquee'); + $canvas.on('mousedown', startDrag); + $canvas.on('mouseup', endDrag); + $canvas.off('mousedown', startMarquee); + $canvas.off('mouseup', endMarquee); + }; + + var stopWatching = function() { + $canvas.off('mousedown', startDrag); + $canvas.off('mouseup', endDrag); + $canvas.off('mousedown', startMarquee); + $canvas.off('mouseup', endMarquee); + window.removeEventListener('keydown', toggleInteractionMode); + window.removeEventListener('keyup', resetInteractionMode); + }; + + var toggleInteractionMode = function(event) { + if (event.keyCode == '18') { // control key. + watchForDrag(); + } + }; + + var resetInteractionMode = function(event) { + if (event.keyCode == '18') { + watchForMarquee(); + } + }; + + $canvas.on('mousemove', trackMousePosition); + $canvas.on('mouseleave', untrackMousePosition); + watchForMarquee(); + + window.addEventListener('keydown', toggleInteractionMode); + window.addEventListener('keyup', resetInteractionMode); + + var onViewportChange = function() { + if ($scope.mouseCoordinates && chartElementBounds) { + $scope.mouseCoordinates.positionAsPlotPoint = + utils.elementPositionAsPlotPosition( + $scope.mouseCoordinates.positionOverElement, + chartElementBounds, + $scope.viewport + ); + } + if (marqueeBox && marqueeBox.start) { + // TODO: Discuss whether marqueeBox start should be fixed to data or fixed to canvas element, especially when "isLive is true". + } + updateAxesForCurrentViewport(); + }; + + $scope.$watchCollection('viewport', onViewportChange); + + $scope.$on('$destroy', stopWatching); + + } + + return { + restrict: "E", + templateUrl: 'platform/features/plot-reborn/res/templates/mct-plot.html', + link: link, + scope: { + viewport: "=", + series: "=", + rectangles: "=", + axes: "=", + displayableRange: "=", + displayableDomain: "=" + } + }; + } + + return MCTPlot; + } +); diff --git a/platform/features/plot-reborn/src/draw/Draw2D.js b/platform/features/plot-reborn/src/draw/Draw2D.js new file mode 100644 index 0000000000..eafd5f2ca0 --- /dev/null +++ b/platform/features/plot-reborn/src/draw/Draw2D.js @@ -0,0 +1,120 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Create a new draw API utilizing the Canvas's 2D API for rendering. + * + * @constructor + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if Canvas's 2D API is unavailable. + */ + function Draw2D(canvas) { + var c2d = canvas.getContext('2d'), + width = canvas.width, + height = canvas.height, + dimensions = [ width, height ], + origin = [ 0, 0 ]; + + // 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. + * @param {number[]} dimensions the horizontal and + * vertical dimensions of the chart + * @param {number[]} origin the horizontal/vertical + * origin of the chart + */ + setDimensions: function (newDimensions, newOrigin) { + dimensions = newDimensions; + origin = newOrigin; + }, + /** + * 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) { + 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 < points * 2; 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, + * 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) { + 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); + } + }; + } + + return Draw2D; + } +); \ No newline at end of file diff --git a/platform/features/plot-reborn/src/draw/DrawLoader.js b/platform/features/plot-reborn/src/draw/DrawLoader.js new file mode 100644 index 0000000000..3f517fe1c8 --- /dev/null +++ b/platform/features/plot-reborn/src/draw/DrawLoader.js @@ -0,0 +1,45 @@ +/*global define */ + +define( + [ + './DrawWebGL', + './Draw2D' + ], + function (DrawWebGL, Draw2D) { + + var CHARTS = [ + DrawWebGL, + Draw2D + ]; + + /** + * Draw loader attaches a draw API to a canvas element and returns the + * draw API. + */ + return { + /** + * Return the first draw API available. Returns + * `undefined` if a draw API could not be constructed. + *. + * @param {CanvasElement} canvas - The canvas eelement to attach + the draw API to. + */ + getDrawAPI: function (canvas) { + for (var i = 0; i < CHARTS.length; i++) { + try { + return new CHARTS[i](canvas); + } catch (e) { + $log.warn([ + "Could not instantiate chart", + CHARTS[i].name, + ";", + e.message + ].join(" ")); + } + } + $log.warn("Cannot initialize mct-chart."); + return undefined; + } + }; + } +); \ No newline at end of file diff --git a/platform/features/plot-reborn/src/draw/DrawWebGL.js b/platform/features/plot-reborn/src/draw/DrawWebGL.js new file mode 100644 index 0000000000..7206251289 --- /dev/null +++ b/platform/features/plot-reborn/src/draw/DrawWebGL.js @@ -0,0 +1,152 @@ +/*global define,Float32Array*/ + +define( + [], + function () { + "use strict"; + + // WebGL shader sources (for drawing plain colors) + var FRAGMENT_SHADER = [ + "precision mediump float;", + "uniform vec4 uColor;", + "void main(void) {", + "gl_FragColor = uColor;", + "}" + ].join('\n'), + VERTEX_SHADER = [ + "attribute vec2 aVertexPosition;", + "uniform vec2 uDimensions;", + "uniform vec2 uOrigin;", + "void main(void) {", + "gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);", + "}" + ].join('\n'); + + /** + * Create a draw api utilizing WebGL. + * + * @constructor + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if WebGL is unavailable. + */ + function DrawWebGL(canvas) { + var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), + vertexShader, + fragmentShader, + program, + aVertexPosition, + uColor, + uDimensions, + uOrigin, + buffer; + + // Ensure a context was actually available before proceeding + if (!gl) { + throw new Error("WebGL unavailable."); + } + + // Initialize shaders + vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, VERTEX_SHADER); + gl.compileShader(vertexShader); + fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, FRAGMENT_SHADER); + gl.compileShader(fragmentShader); + + // Assemble vertex/fragment shaders into programs + program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + gl.useProgram(program); + + // Get locations for attribs/uniforms from the + // shader programs (to pass values into shaders at draw-time) + aVertexPosition = gl.getAttribLocation(program, "aVertexPosition"); + uColor = gl.getUniformLocation(program, "uColor"); + uDimensions = gl.getUniformLocation(program, "uDimensions"); + uOrigin = gl.getUniformLocation(program, "uOrigin"); + gl.enableVertexAttribArray(aVertexPosition); + + // Create a buffer to holds points which will be drawn + buffer = gl.createBuffer(); + + // Use a line width of 2.0 for legibility + gl.lineWidth(2.0); + + // Enable blending, for smoothness + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Utility function to handle drawing of a buffer; + // drawType will determine whether this is a box, line, etc. + function doDraw(drawType, buf, color, points) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0); + gl.uniform4fv(uColor, color); + gl.drawArrays(drawType, 0, points); + } + + return { + /** + * Clear the chart. + */ + clear: function () { + // Set the viewport size; note that we use the width/height + // that our WebGL context reports, which may be lower + // resolution than the canvas we requested. + gl.viewport( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight + ); + gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT); + }, + /** + * 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) { + if (dimensions && dimensions.length > 0 && + origin && origin.length > 0) { + gl.uniform2fv(uDimensions, dimensions); + gl.uniform2fv(uOrigin, 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) { + doDraw(gl.LINE_STRIP, 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) { + doDraw(gl.TRIANGLE_FAN, new Float32Array( + min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) + ), color, 4); + } + }; + } + return DrawWebGL; + } +); diff --git a/platform/features/plot-reborn/src/lib/utils.js b/platform/features/plot-reborn/src/lib/utils.js new file mode 100644 index 0000000000..18f7ecf1f7 --- /dev/null +++ b/platform/features/plot-reborn/src/lib/utils.js @@ -0,0 +1,75 @@ +/*global define*/ + +define(function() { + "use strict"; + + var utils = {}; + + utils.boxPointsFromOppositeCorners = function(start, end) { + // Given two points defining opposite corners of a square, + // return an array of points containing all of the boxes' rectangles. + return [ + start, + {domain: start.domain, range: end.range}, + end, + {domain: end.domain, range: start.range} + ]; + }; + + utils.oppositeCornersFromBoxPoints = function(boxPoints) { + // Given an array of box points, return the topLeft and bottomRight points of the box. + var topLeft = boxPoints.reduce(function(topLeft, currentPoint) { + if (!topLeft) { + return currentPoint; + } + if (currentPoint.domain <= topLeft.domain && + currentPoint.range >= topLeft.range) { + return currentPoint; + } + return topLeft; + }); + + var bottomRight = boxPoints.reduce(function(bottomRight, currentPoint) { + if (!bottomRight) { + return currentPoint; + } + if (currentPoint.domain >= bottomRight.domain && + currentPoint.range <= bottomRight.range) { + return currentPoint; + } + return bottomRight; + }); + + return { + topLeft: topLeft, + bottomRight: bottomRight + }; + }; + + utils.elementPositionAsPlotPosition = function(elementPosition, elementBounds, viewport) { + // Convert an (x, y) pair in element space to a + // (domain, range) pair viewport. + + // Element space has (0,0) as the topLeft corner, With x + // increasing to the right and y increasing to the bottom. + + var maxDomain = viewport.bottomRight.domain; + var minDomain = viewport.topLeft.domain; + var domainDenominator = maxDomain - minDomain; + + var maxRange = viewport.topLeft.range; + var minRange = viewport.bottomRight.range; + var rangeDenominator = maxRange - minRange; + + var xFraction = elementPosition.x / elementBounds.width; + var yFraction = elementPosition.y / elementBounds.height; + + return { + domain: minDomain + domainDenominator * xFraction, + range: maxRange - rangeDenominator * yFraction + }; + }; + + + return utils; +}); From bb8c8a75abb80ddeb5facd71990b3db3723cbd2c Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Aug 2015 13:59:32 -0700 Subject: [PATCH 2/9] MCTChart preserves precision of plot values --- .../src/controllers/PlotController.js | 11 +-- .../plot-reborn/src/directives/MCTChart.js | 88 +++++++++++++++---- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/platform/features/plot-reborn/src/controllers/PlotController.js b/platform/features/plot-reborn/src/controllers/PlotController.js index 4889ea004a..afdd90e9a4 100644 --- a/platform/features/plot-reborn/src/controllers/PlotController.js +++ b/platform/features/plot-reborn/src/controllers/PlotController.js @@ -11,10 +11,10 @@ define( function PlotController($scope) { var plotHistory = []; var isLive = true; - var maxDomain = 0; - var domainOffset = +new Date(); + var maxDomain = +new Date(); var subscriptions = []; var setToDefaultViewport = function() { + // TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport. $scope.viewport = { topLeft: { domain: maxDomain - DOMAIN_INTERVAL, @@ -35,7 +35,7 @@ define( }; $scope.displayableDomain = function(domainValue) { // TODO: Call format function provided by domain object. - return new Date(domainValue + domainOffset).toUTCString(); + return new Date(domainValue).toUTCString(); }; $scope.series = []; @@ -43,10 +43,7 @@ define( $scope.rectangles = []; var updateSeriesFromTelemetry = function(series, seriesIndex, telemetry) { - if (typeof domainOffset === 'undefined') { - domainOffset = telemetry.getDomainValue(telemetry.getPointCount() - 1); - } - var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1) - domainOffset; + var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1); var rangeValue = telemetry.getRangeValue(telemetry.getPointCount() - 1); // Track the biggest domain we've seen for sticky-ness. maxDomain = Math.max(maxDomain, domainValue); diff --git a/platform/features/plot-reborn/src/directives/MCTChart.js b/platform/features/plot-reborn/src/directives/MCTChart.js index 7b88a43243..4a9ba6b574 100644 --- a/platform/features/plot-reborn/src/directives/MCTChart.js +++ b/platform/features/plot-reborn/src/directives/MCTChart.js @@ -10,6 +10,26 @@ define( var TEMPLATE = ""; + /** + * Offsetter adjusts domain and range values by a fixed amount, + * generally increasing the precision of the 32 bit float representation + * required for plotting. + * + * @constructor + */ + function Offsetter(domainOffset, rangeOffset) { + this.domainOffset = domainOffset; + this.rangeOffset = rangeOffset; + } + + Offsetter.prototype.domain = function(dataDomain) { + return dataDomain - this.domainOffset; + }; + + Offsetter.prototype.range = function(dataRange) { + return dataRange - this.rangeOffset; + }; + /** * MCTChart draws charts utilizing a drawAPI. * @@ -22,7 +42,8 @@ define( isDestroyed = false, activeInterval, drawAPI, - lines = []; + lines = [], + offset; drawAPI = DrawLoader.getDrawAPI(canvas); @@ -34,15 +55,35 @@ define( if (isDestroyed) { return; } + requestAnimationFrame(redraw); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; drawAPI.clear(); + createOffset(); + if (!offset) { + return; + } updateViewport(); drawSeries(); drawRectangles(); } + function createOffset() { + if (offset) { + return; + } + if (!$scope.viewport || + !$scope.viewport.topLeft || + !$scope.viewport.bottomRight) { + return; + } + offset = new Offsetter( + $scope.viewport.topLeft.domain, + $scope.viewport.topLeft.range + ); + } + function drawIfResized() { if (canvas.width !== canvas.offsetWidth || canvas.height !== canvas.offsetHeight) { @@ -59,6 +100,9 @@ define( function drawSeries() { // TODO: Don't regenerate lines on each frame. + if (!$scope.series || !$scope.series.length) { + return; + } lines = $scope.series.map(lineFromSeries); lines.forEach(function(line) { drawAPI.drawLine( @@ -73,8 +117,14 @@ define( if ($scope.rectangles) { $scope.rectangles.forEach(function(rect) { drawAPI.drawSquare( - [rect.start.domain, rect.start.range], - [rect.end.domain, rect.end.range], + [ + offset.domain(rect.start.domain), + offset.range(rect.start.range) + ], + [ + offset.domain(rect.end.domain), + offset.range(rect.end.range) + ], rect.color ); }); @@ -83,13 +133,23 @@ define( function updateViewport() { var dimensions = [ - Math.abs($scope.viewport.topLeft.domain - $scope.viewport.bottomRight.domain), - Math.abs($scope.viewport.topLeft.range - $scope.viewport.bottomRight.range) + Math.abs( + offset.domain($scope.viewport.topLeft.domain) - + offset.domain($scope.viewport.bottomRight.domain) + ), + Math.abs( + offset.range($scope.viewport.topLeft.range) - + offset.range($scope.viewport.bottomRight.range) + ) ]; var origin = [ - $scope.viewport.topLeft.domain, - $scope.viewport.bottomRight.range + offset.domain( + $scope.viewport.topLeft.domain + ), + offset.range( + $scope.viewport.bottomRight.range + ) ]; drawAPI.setDimensions( @@ -113,8 +173,8 @@ define( // appears minimal. var lineBuffer = new Float32Array(20000); for (var i = 0; i < series.data.length; i++) { - lineBuffer[2*i] = series.data[i].domain; - lineBuffer[2*i+1] = series.data[i].range; + lineBuffer[2*i] = offset.domain(series.data[i].domain); + lineBuffer[2*i+1] = offset.range(series.data[i].range); } return { color: series.color, @@ -123,15 +183,11 @@ define( }; } - function initializeLines() { - lines = $scope.series.map(lineFromSeries); - } - function onSeriesDataAdd(event, seriesIndex, points) { var line = lines[seriesIndex]; points.forEach(function (point) { - line.buffer[2*line.pointCount] = point.domain; - line.buffer[2*line.pointCount+1] = point.range; + line.buffer[2*line.pointCount] = offset.domain(point.domain); + line.buffer[2*line.pointCount+1] = offset.range(point.range); line.pointCount += 1; }); } @@ -139,8 +195,6 @@ define( // Check for resize, on a timer activeInterval = $interval(drawIfResized, 1000); - // Initialize series - $scope.$watch('series', initializeLines); $scope.$on('series:data:add', onSeriesDataAdd); redraw(); From 52b8720d370a5d95444cb7abf9a2b459769a3b8d Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Aug 2015 16:17:20 -0700 Subject: [PATCH 3/9] ColorPalette dispenses colors. --- .../features/plot-reborn/src/ColorPalette.js | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 platform/features/plot-reborn/src/ColorPalette.js diff --git a/platform/features/plot-reborn/src/ColorPalette.js b/platform/features/plot-reborn/src/ColorPalette.js new file mode 100644 index 0000000000..ddc3b23476 --- /dev/null +++ b/platform/features/plot-reborn/src/ColorPalette.js @@ -0,0 +1,136 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define*/ +define( + function () { + 'use strict'; + + var COLOR_PALETTE = [ + [ 0x20, 0xB2, 0xAA ], + [ 0x9A, 0xCD, 0x32 ], + [ 0xFF, 0x8C, 0x00 ], + [ 0xD2, 0xB4, 0x8C ], + [ 0x40, 0xE0, 0xD0 ], + [ 0x41, 0x69, 0xFF ], + [ 0xFF, 0xD7, 0x00 ], + [ 0x6A, 0x5A, 0xCD ], + [ 0xEE, 0x82, 0xEE ], + [ 0xCC, 0x99, 0x66 ], + [ 0x99, 0xCC, 0xCC ], + [ 0x66, 0xCC, 0x33 ], + [ 0xFF, 0xCC, 0x00 ], + [ 0xFF, 0x66, 0x33 ], + [ 0xCC, 0x66, 0xFF ], + [ 0xFF, 0x00, 0x66 ], + [ 0xFF, 0xFF, 0x00 ], + [ 0x80, 0x00, 0x80 ], + [ 0x00, 0x86, 0x8B ], + [ 0x00, 0x8A, 0x00 ], + [ 0xFF, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFF ], + [ 0xF5, 0xDE, 0xB3 ], + [ 0xBC, 0x8F, 0x8F ], + [ 0x46, 0x82, 0xB4 ], + [ 0xFF, 0xAF, 0xAF ], + [ 0x43, 0xCD, 0x80 ], + [ 0xCD, 0xC1, 0xC5 ], + [ 0xA0, 0x52, 0x2D ], + [ 0x64, 0x95, 0xED ] + ]; + + /** + * A representation of a color that allows conversions between different + * formats. + * + * @constructor + */ + function Color(integerArray) { + this.integerArray = integerArray; + } + + /** + * Return color as a three element array of RGB values, where each value + * is a integer in the range of 0-255. + * + * @return {number[]} the color, as integer RGB values + */ + Color.prototype.asIntegerArray = function () { + return this.integerArray.map(function (c) { + return c; + }); + }; + + /** + * Return color as a string using #-prefixed six-digit RGB hex notation + * (e.g. #FF0000). See http://www.w3.org/TR/css3-color/#rgb-color. + * + * @return {string} the color, as a style-friendly string + */ + + Color.prototype.asHexString = function () { + return '#' + this.integerArray.map(function (c) { + return (c < 16 ? '0' : '') + c.toString(16); + }).join(''); + }; + + /** + * Return color as a RGBA float array. + * + * This format is present specifically to support use with + * WebGL, which expects colors of that form. + * + * @return {number[]} the color, as floating-point RGBA values + */ + Color.prototype.asRGBAArray = function () { + return this.integerArray.map(function (c) { + return c / 255.0; + }).concat([1]); + }; + + /** + * A color palette which returns colors from a predefined palette. + * When all colors in the palette have been allocated, it starts reusing + * colors. + * + * @constructor + */ + function ColorPalette() { + this.nextColor = 0; + this.colors = COLOR_PALETTE.map(function (color) { + return new Color(color); + }); + } + + /** + * + * @return {Color} the next color in the palette. + */ + ColorPalette.prototype.getColor = function () { + var color = this.colors[this.nextColor % this.colors.length]; + this.nextColor++; + return color; + }; + + return ColorPalette; + } +); From cb41be79229780fa362c055f4cfe72f2050ca38f Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Aug 2015 16:27:28 -0700 Subject: [PATCH 4/9] Plot uses ColorPalette to allocate colors --- platform/features/plot-reborn/res/templates/mct-plot.html | 2 +- .../features/plot-reborn/src/controllers/PlotController.js | 6 ++++-- platform/features/plot-reborn/src/directives/MCTChart.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/platform/features/plot-reborn/res/templates/mct-plot.html b/platform/features/plot-reborn/res/templates/mct-plot.html index c3b3096f83..3311d70026 100644 --- a/platform/features/plot-reborn/res/templates/mct-plot.html +++ b/platform/features/plot-reborn/res/templates/mct-plot.html @@ -4,7 +4,7 @@ + ng-style="{ 'background-color': series.color.asHexString() }"> {{ series.name }} diff --git a/platform/features/plot-reborn/src/controllers/PlotController.js b/platform/features/plot-reborn/src/controllers/PlotController.js index afdd90e9a4..9b9d613e8a 100644 --- a/platform/features/plot-reborn/src/controllers/PlotController.js +++ b/platform/features/plot-reborn/src/controllers/PlotController.js @@ -1,7 +1,8 @@ /*global define*/ define( - function () { + ['../ColorPalette.js'], + function (ColorPalette) { "use strict"; // TODO: Store this in more accessible locations / retrieve from @@ -13,6 +14,7 @@ define( var isLive = true; var maxDomain = +new Date(); var subscriptions = []; + var palette = new ColorPalette(); var setToDefaultViewport = function() { // TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport. $scope.viewport = { @@ -63,7 +65,7 @@ define( var series = { name: model.name, // TODO: Bring back PlotPalette. - color: [0.12549019607843137, 0.6980392156862745, 0.6666666666666666, 1], + color: palette.getColor(), data: [] }; diff --git a/platform/features/plot-reborn/src/directives/MCTChart.js b/platform/features/plot-reborn/src/directives/MCTChart.js index 4a9ba6b574..ff3206f271 100644 --- a/platform/features/plot-reborn/src/directives/MCTChart.js +++ b/platform/features/plot-reborn/src/directives/MCTChart.js @@ -107,7 +107,7 @@ define( lines.forEach(function(line) { drawAPI.drawLine( line.buffer, - line.color, + line.color.asRGBAArray(), line.pointCount ); }); From 5502009127ef6ff55b21d84c6c427a37450060ba Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Aug 2015 16:50:09 -0700 Subject: [PATCH 5/9] Reasonable defaults for directive scope --- .../plot-reborn/src/directives/MCTPlot.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/platform/features/plot-reborn/src/directives/MCTPlot.js b/platform/features/plot-reborn/src/directives/MCTPlot.js index 20832ce1d3..b26d3728a3 100644 --- a/platform/features/plot-reborn/src/directives/MCTPlot.js +++ b/platform/features/plot-reborn/src/directives/MCTPlot.js @@ -14,18 +14,31 @@ define( function link($scope, $element) { // Now that we're here, let's handle some scope management that the controller would otherwise handle. - $scope.axes = { - domain: { - label: "Time", - tickCount: DOMAIN_TICK_COUNT, - ticks: [] - }, - range: { - label: "Value", - tickCount: RANGE_TICK_COUNT, - ticks: [] - } - }; + + if (typeof $scope.rectangles === "undefined") { + $scope.rectangles = []; + } + if (typeof $scope.displayableRange === "undefined") { + $scope.displayableRange = function (x) { return x; }; + } + if (typeof $scope.displayableDomain === "undefined") { + $scope.displayableDomain = function (x) { return x; }; + } + if (typeof $scope.axes === "undefined") { + $scope.axes = { + domain: { + label: "Time", + tickCount: DOMAIN_TICK_COUNT, + ticks: [] + }, + range: { + label: "Value", + tickCount: RANGE_TICK_COUNT, + ticks: [] + } + }; + } + var dragStart; var marqueeBox = {}; @@ -241,10 +254,10 @@ define( scope: { viewport: "=", series: "=", - rectangles: "=", - axes: "=", - displayableRange: "=", - displayableDomain: "=" + rectangles: "=?", + axes: "=?", + displayableRange: "=?", + displayableDomain: "=?" } }; } From 889a5c6ea95ccb9e42d9f49228f3c79100b76076 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Aug 2015 16:50:51 -0700 Subject: [PATCH 6/9] Allow duplicates in repeats, use proper labels --- .../plot-reborn/res/templates/mct-plot.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/platform/features/plot-reborn/res/templates/mct-plot.html b/platform/features/plot-reborn/res/templates/mct-plot.html index 3311d70026..0fdd6a7c3a 100644 --- a/platform/features/plot-reborn/res/templates/mct-plot.html +++ b/platform/features/plot-reborn/res/templates/mct-plot.html @@ -2,7 +2,7 @@
+ ng-repeat="series in series track by $index"> @@ -20,10 +20,10 @@
- {{ axes.range.name}} + {{ axes.range.label}}
-
@@ -34,14 +34,14 @@
@@ -62,7 +62,7 @@
-
@@ -70,7 +70,7 @@
- {{ axes.domain.name }} + {{ axes.domain.label }}
From 48f345a46bf59746689a81692395a0bdab0ebd7f Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Thu, 13 Aug 2015 16:06:26 -0700 Subject: [PATCH 7/9] [Service] ColorPalette is now ColorService --- platform/features/plot-reborn/bundle.json | 7 ++++ .../ColorService.js} | 32 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) rename platform/features/plot-reborn/src/{ColorPalette.js => services/ColorService.js} (82%) diff --git a/platform/features/plot-reborn/bundle.json b/platform/features/plot-reborn/bundle.json index b526ea0323..109aaaa26e 100644 --- a/platform/features/plot-reborn/bundle.json +++ b/platform/features/plot-reborn/bundle.json @@ -84,6 +84,13 @@ "model": {"composition": []}, "properties": [] } + ], + "services": [ + { + "key": "colorService", + "implementation": "services/ColorService.js", + "description": "Provides objects for working with colors." + } ] } } diff --git a/platform/features/plot-reborn/src/ColorPalette.js b/platform/features/plot-reborn/src/services/ColorService.js similarity index 82% rename from platform/features/plot-reborn/src/ColorPalette.js rename to platform/features/plot-reborn/src/services/ColorService.js index ddc3b23476..207fd91f72 100644 --- a/platform/features/plot-reborn/src/ColorPalette.js +++ b/platform/features/plot-reborn/src/services/ColorService.js @@ -108,9 +108,8 @@ define( }; /** - * A color palette which returns colors from a predefined palette. - * When all colors in the palette have been allocated, it starts reusing - * colors. + * A color palette stores a set of colors and allows for different + * methods of color allocation. * * @constructor */ @@ -122,15 +121,34 @@ define( } /** - * - * @return {Color} the next color in the palette. + * @returns {Color} the next unused color in the palette. If all colors + * have been allocated, it will wrap around. */ - ColorPalette.prototype.getColor = function () { + ColorPalette.prototype.getNextColor = function () { var color = this.colors[this.nextColor % this.colors.length]; this.nextColor++; return color; }; - return ColorPalette; + /** + * @param {number} index the index of the color to return. An index + * value larger than the size of the index will wrap around. + * @returns {Color} + */ + ColorPalette.prototype.getColor = function (index) { + return this.colors[index % this.colors.length]; + }; + + + function ColorService() {} + + ColorService.prototype.ColorPalette = ColorPalette; + ColorService.prototype.Color = Color; + + function getColorService() { + return new ColorService(); + } + + return getColorService; } ); From a0dc3da8fb12fb3b2df1920cc55c5d904c637700 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Thu, 13 Aug 2015 16:07:08 -0700 Subject: [PATCH 8/9] [Plot] Use ColorService for plot colors --- platform/features/plot-reborn/bundle.json | 4 ++-- .../plot-reborn/src/controllers/PlotController.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/platform/features/plot-reborn/bundle.json b/platform/features/plot-reborn/bundle.json index 109aaaa26e..f8f6c6b7c2 100644 --- a/platform/features/plot-reborn/bundle.json +++ b/platform/features/plot-reborn/bundle.json @@ -53,12 +53,12 @@ { "key": "PlotController", "implementation": "controllers/PlotController.js", - "depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ] + "depends": [ "$scope", "colorService"] }, { "key": "StackedPlotController", "implementation": "controllers/StackedPlotController.js", - "depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ] + "depends": [ "$scope" ] } ], "types": [ diff --git a/platform/features/plot-reborn/src/controllers/PlotController.js b/platform/features/plot-reborn/src/controllers/PlotController.js index 9b9d613e8a..4bf74fac6c 100644 --- a/platform/features/plot-reborn/src/controllers/PlotController.js +++ b/platform/features/plot-reborn/src/controllers/PlotController.js @@ -1,20 +1,20 @@ /*global define*/ define( - ['../ColorPalette.js'], - function (ColorPalette) { + [], + function () { "use strict"; // TODO: Store this in more accessible locations / retrieve from // domainObject metadata. var DOMAIN_INTERVAL = 1 * 60 * 1000; // One minute. - function PlotController($scope) { + function PlotController($scope, colorService) { var plotHistory = []; var isLive = true; var maxDomain = +new Date(); var subscriptions = []; - var palette = new ColorPalette(); + var palette = new colorService.ColorPalette(); var setToDefaultViewport = function() { // TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport. $scope.viewport = { @@ -65,7 +65,7 @@ define( var series = { name: model.name, // TODO: Bring back PlotPalette. - color: palette.getColor(), + color: palette.getColor($scope.series.length), data: [] }; From 056b3f61ce78f35ac72999aa3bd12ffd9e7310d9 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Fri, 14 Aug 2015 11:39:05 -0700 Subject: [PATCH 9/9] [Style] JSLint Compliance --- .../src/controllers/PlotController.js | 95 ++++---- .../plot-reborn/src/directives/MCTChart.js | 119 +++++----- .../plot-reborn/src/directives/MCTPlot.js | 217 +++++++++--------- .../plot-reborn/src/draw/DrawLoader.js | 7 +- 4 files changed, 228 insertions(+), 210 deletions(-) diff --git a/platform/features/plot-reborn/src/controllers/PlotController.js b/platform/features/plot-reborn/src/controllers/PlotController.js index 4bf74fac6c..fe0b17f05d 100644 --- a/platform/features/plot-reborn/src/controllers/PlotController.js +++ b/platform/features/plot-reborn/src/controllers/PlotController.js @@ -7,15 +7,17 @@ define( // TODO: Store this in more accessible locations / retrieve from // domainObject metadata. - var DOMAIN_INTERVAL = 1 * 60 * 1000; // One minute. + var DOMAIN_INTERVAL = 2 * 60 * 1000; // Two minutes. function PlotController($scope, colorService) { - var plotHistory = []; - var isLive = true; - var maxDomain = +new Date(); - var subscriptions = []; - var palette = new colorService.ColorPalette(); - var setToDefaultViewport = function() { + var plotHistory = [], + isLive = true, + maxDomain = +new Date(), + subscriptions = [], + palette = new colorService.ColorPalette(); + + + function setToDefaultViewport() { // TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport. $scope.viewport = { topLeft: { @@ -27,15 +29,15 @@ define( range: -1 } }; - }; + } setToDefaultViewport(); - $scope.displayableRange = function(rangeValue) { + $scope.displayableRange = function (rangeValue) { // TODO: Call format function provided by domain object. return rangeValue; }; - $scope.displayableDomain = function(domainValue) { + $scope.displayableDomain = function (domainValue) { // TODO: Call format function provided by domain object. return new Date(domainValue).toUTCString(); }; @@ -44,25 +46,33 @@ define( $scope.rectangles = []; - var updateSeriesFromTelemetry = function(series, seriesIndex, telemetry) { - var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1); - var rangeValue = telemetry.getRangeValue(telemetry.getPointCount() - 1); + function updateSeriesFromTelemetry(series, seriesIndex, telemetry) { + var domainValue = telemetry.getDomainValue( + telemetry.getPointCount() - 1 + ), + rangeValue = telemetry.getRangeValue( + telemetry.getPointCount() - 1 + ), + newTelemetry; // Track the biggest domain we've seen for sticky-ness. maxDomain = Math.max(maxDomain, domainValue); - var newTelemetry = { + newTelemetry = { domain: domainValue, range: rangeValue }; series.data.push(newTelemetry); $scope.$broadcast('series:data:add', seriesIndex, [newTelemetry]); - }; + } - var subscribeToDomainObject = function(domainObject) { - var telemetryCapability = domainObject.getCapability('telemetry'); - var model = domainObject.getModel(); + function subscribeToDomainObject(domainObject) { + var telemetryCapability = domainObject.getCapability('telemetry'), + model = domainObject.getModel(), + series, + seriesIndex, + updater; - var series = { + series = { name: model.name, // TODO: Bring back PlotPalette. color: palette.getColor($scope.series.length), @@ -70,17 +80,25 @@ define( }; $scope.series.push(series); - var seriesIndex = $scope.series.indexOf(series); + seriesIndex = $scope.series.indexOf(series); - var updater = updateSeriesFromTelemetry.bind( + updater = updateSeriesFromTelemetry.bind( null, series, seriesIndex ); subscriptions.push(telemetryCapability.subscribe(updater)); - }; + } - var linkDomainObject = function(domainObject) { + function unlinkDomainObject() { + subscriptions.forEach(function(subscription) { + subscription.unsubscribe(); + }); + subscriptions = []; + } + + + function linkDomainObject(domainObject) { unlinkDomainObject(); if (domainObject.hasCapability('telemetry')) { subscribeToDomainObject(domainObject); @@ -98,23 +116,17 @@ define( } else { throw new Error('Domain object type not supported.'); } - }; + } - var unlinkDomainObject = function() { - subscriptions.forEach(function(subscription) { - subscription.unsubscribe(); - }); - subscriptions = []; - }; - var onUserViewportChangeStart = function() { + function onUserViewportChangeStart() { // TODO: this is a great time to track a history entry. // Disable live mode so they have full control of viewport. plotHistory.push($scope.viewport); isLive = false; - }; + } - var onUserViewportChangeEnd = function(event, viewport) { + function onUserViewportChangeEnd(event, viewport) { // If the new viewport is "close enough" to the maxDomain then // enable live mode. Set empirically to 10% of the domain // interval. @@ -127,10 +139,10 @@ define( isLive = false; } plotHistory.push(viewport); - }; + } - var viewportForMaxDomain = function() { - var viewport = { + function viewportForMaxDomain() { + return { topLeft: { range: $scope.viewport.topLeft.range, domain: maxDomain - DOMAIN_INTERVAL @@ -140,14 +152,13 @@ define( domain: maxDomain } }; - return viewport; - }; + } - var followDataIfLive = function() { + function followDataIfLive() { if (isLive) { $scope.viewport = viewportForMaxDomain(); } - }; + } $scope.$on('series:data:add', followDataIfLive); $scope.$on('user:viewport:change:end', onUserViewportChangeEnd); @@ -155,7 +166,7 @@ define( $scope.$watch('domainObject', linkDomainObject); - var controller = { + return { historyBack: function() { // TODO: Step History Back. }, @@ -166,8 +177,6 @@ define( // TODO: Reset view to defaults. Keep history stack alive? } }; - - return controller; } return PlotController; diff --git a/platform/features/plot-reborn/src/directives/MCTChart.js b/platform/features/plot-reborn/src/directives/MCTChart.js index ff3206f271..37b39ac11f 100644 --- a/platform/features/plot-reborn/src/directives/MCTChart.js +++ b/platform/features/plot-reborn/src/directives/MCTChart.js @@ -51,24 +51,6 @@ define( return; } - function redraw() { - if (isDestroyed) { - return; - } - - requestAnimationFrame(redraw); - canvas.width = canvas.offsetWidth; - canvas.height = canvas.offsetHeight; - drawAPI.clear(); - createOffset(); - if (!offset) { - return; - } - updateViewport(); - drawSeries(); - drawRectangles(); - } - function createOffset() { if (offset) { return; @@ -84,18 +66,30 @@ define( ); } - function drawIfResized() { - if (canvas.width !== canvas.offsetWidth || - canvas.height !== canvas.offsetHeight) { - redraw(); - } - } - - function destroyChart() { - isDestroyed = true; - if (activeInterval) { - $interval.cancel(activeInterval); + function lineFromSeries(series) { + // TODO: handle when lines get longer than 10,000 points. + // Each line allocates 10,000 points. This should be more + // that we ever need, but we have to decide how to handle + // this at the higher level. I imagine the plot controller + // should watch it's series and when they get huge, slice + // them in half and delete the oldest half. + // + // As long as the controller replaces $scope.series with a + // new series object, then this directive will + // automatically generate new arrays for those lines. + // In practice, the overhead of regenerating these lines + // appears minimal. + var lineBuffer = new Float32Array(20000), + i = 0; + for (i = 0; i < series.data.length; i++) { + lineBuffer[2*i] = offset.domain(series.data[i].domain); + lineBuffer[2*i+1] = offset.range(series.data[i].range); } + return { + color: series.color, + buffer: lineBuffer, + pointCount: series.data.length + }; } function drawSeries() { @@ -132,7 +126,10 @@ define( } function updateViewport() { - var dimensions = [ + var dimensions, + origin; + + dimensions = [ Math.abs( offset.domain($scope.viewport.topLeft.domain) - offset.domain($scope.viewport.bottomRight.domain) @@ -143,7 +140,7 @@ define( ) ]; - var origin = [ + origin = [ offset.domain( $scope.viewport.topLeft.domain ), @@ -158,31 +155,6 @@ define( ); } - function lineFromSeries(series) { - // TODO: handle when lines get longer than 10,000 points. - // Each line allocates 10,000 points. This should be more - // that we ever need, but we have to decide how to handle - // this at the higher level. I imagine the plot controller - // should watch it's series and when they get huge, slice - // them in half and delete the oldest half. - // - // As long as the controller replaces $scope.series with a - // new series object, then this directive will - // automatically generate new arrays for those lines. - // In practice, the overhead of regenerating these lines - // appears minimal. - var lineBuffer = new Float32Array(20000); - for (var i = 0; i < series.data.length; i++) { - lineBuffer[2*i] = offset.domain(series.data[i].domain); - lineBuffer[2*i+1] = offset.range(series.data[i].range); - } - return { - color: series.color, - buffer: lineBuffer, - pointCount: series.data.length - }; - } - function onSeriesDataAdd(event, seriesIndex, points) { var line = lines[seriesIndex]; points.forEach(function (point) { @@ -192,6 +164,41 @@ define( }); } + + + function redraw() { + if (isDestroyed) { + return; + } + + requestAnimationFrame(redraw); + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + drawAPI.clear(); + createOffset(); + if (!offset) { + return; + } + updateViewport(); + drawSeries(); + drawRectangles(); + } + + + function drawIfResized() { + if (canvas.width !== canvas.offsetWidth || + canvas.height !== canvas.offsetHeight) { + redraw(); + } + } + + function destroyChart() { + isDestroyed = true; + if (activeInterval) { + $interval.cancel(activeInterval); + } + } + // Check for resize, on a timer activeInterval = $interval(drawIfResized, 1000); diff --git a/platform/features/plot-reborn/src/directives/MCTPlot.js b/platform/features/plot-reborn/src/directives/MCTPlot.js index b26d3728a3..cef2126935 100644 --- a/platform/features/plot-reborn/src/directives/MCTPlot.js +++ b/platform/features/plot-reborn/src/directives/MCTPlot.js @@ -7,8 +7,8 @@ define( function (utils) { "use strict"; - var RANGE_TICK_COUNT = 7; - var DOMAIN_TICK_COUNT = 5; + var RANGE_TICK_COUNT = 7, + DOMAIN_TICK_COUNT = 5; function MCTPlot() { @@ -40,34 +40,37 @@ define( } - var dragStart; - var marqueeBox = {}; - var marqueeRect; // Set when exists. - var chartElementBounds; - var $canvas = $element.find('canvas'); + var dragStart, + marqueeBox = {}, + marqueeRect, // Set when exists. + chartElementBounds, + $canvas = $element.find('canvas'); - var updateAxesForCurrentViewport = function() { + function updateAxesForCurrentViewport() { // Update axes definitions for current viewport. ['domain', 'range'].forEach(function(axisName) { - var axis = $scope.axes[axisName]; - var firstTick = $scope.viewport.topLeft[axisName]; - var lastTick = $scope.viewport.bottomRight[axisName]; - var axisSize = firstTick - lastTick; - var denominator = axis.tickCount - 1; + var axis = $scope.axes[axisName], + firstTick = $scope.viewport.topLeft[axisName], + lastTick = $scope.viewport.bottomRight[axisName], + axisSize = firstTick - lastTick, + denominator = axis.tickCount - 1, + tickNumber, + tickIncrement, + tickValue; // Yes, ticksize is negative for domain and positive for range. // It's because ticks are generated/displayed top to bottom and left to right. axis.ticks = []; - for (var tickNumber = 0; tickNumber < axis.tickCount; tickNumber++) { - var tickIncrement = (axisSize * (tickNumber / denominator)); - var tickValue = firstTick - tickIncrement; + for (tickNumber = 0; tickNumber < axis.tickCount; tickNumber++) { + tickIncrement = (axisSize * (tickNumber / denominator)); + tickValue = firstTick - tickIncrement; axis.ticks.push( tickValue ); } }); - }; + } - var drawMarquee = function() { + function drawMarquee() { // Create rectangle for Marquee if it should be set. if (marqueeBox && marqueeBox.start && marqueeBox.end) { if (!marqueeRect) { @@ -79,30 +82,88 @@ define( marqueeRect.color = [1, 1, 1, 0.5]; marqueeRect.layer = 'top'; // TODO: implement this. $scope.$broadcast('rectangle-change'); - } else if (marqueeRect && $scope.rectangles.indexOf(marqueeRect) != -1) { + } else if (marqueeRect && $scope.rectangles.indexOf(marqueeRect) !== -1) { $scope.rectangles.splice($scope.rectangles.indexOf(marqueeRect)); marqueeRect = undefined; $scope.$broadcast('rectangle-change'); } - }; + } - var untrackMousePosition = function() { + function untrackMousePosition() { $scope.mouseCoordinates = undefined; - }; + } + function updateMarquee() { + // Update the marquee box in progress. + marqueeBox.end = $scope.mouseCoordinates.positionAsPlotPoint; + drawMarquee(); + } + function startMarquee() { + marqueeBox.start = $scope.mouseCoordinates.positionAsPlotPoint; + } + function endMarquee() { + // marqueeBox start/end are opposite corners but we need + // topLeft and bottomRight. + var boxPoints = utils.boxPointsFromOppositeCorners(marqueeBox.start, marqueeBox.end), + newViewport = utils.oppositeCornersFromBoxPoints(boxPoints); - var trackMousePosition = function($event) { + marqueeBox = {}; + drawMarquee(); + $scope.$emit('user:viewport:change:end', newViewport); + $scope.viewport = newViewport; + } + + function startDrag($event) { + $scope.$emit('user:viewport:change:start'); + if (!$scope.mouseCoordinates) { + return; + } + $event.preventDefault(); + // Track drag location relative to position over element + // not domain, as chart viewport will change as we drag. + dragStart = $scope.mouseCoordinates.positionAsPlotPoint; + // Tell controller that we're starting to navigate. + return false; + } + + function updateDrag() { + // calculate offset between points. Apply that offset to viewport. + var newPosition = $scope.mouseCoordinates.positionAsPlotPoint, + dDomain = dragStart.domain - newPosition.domain, + dRange = dragStart.range - newPosition.range; + + $scope.viewport = { + topLeft: { + domain: $scope.viewport.topLeft.domain + dDomain, + range: $scope.viewport.topLeft.range + dRange + }, + bottomRight: { + domain: $scope.viewport.bottomRight.domain + dDomain, + range: $scope.viewport.bottomRight.range + dRange + } + }; + } + + function endDrag() { + dragStart = undefined; + $scope.$emit('user:viewport:change:end', $scope.viewport); + } + + function trackMousePosition($event) { // Calculate coordinates of mouse related to canvas and as // domain, range value and make available in scope for display. - var bounds = $event.target.getBoundingClientRect(); + var bounds = $event.target.getBoundingClientRect(), + positionOverElement, + positionAsPlotPoint; + chartElementBounds = bounds; - var positionOverElement = { + positionOverElement = { x: $event.clientX - bounds.left, y: $event.clientY - bounds.top }; - var positionAsPlotPoint = utils.elementPositionAsPlotPosition( + positionAsPlotPoint = utils.elementPositionAsPlotPosition( positionOverElement, bounds, $scope.viewport @@ -120,104 +181,46 @@ define( if (dragStart) { updateDrag(); } - }; + } - var startMarquee = function() { - marqueeBox.start = $scope.mouseCoordinates.positionAsPlotPoint; - }; - - var updateMarquee = function() { - // Update the marquee box in progress. - marqueeBox.end = $scope.mouseCoordinates.positionAsPlotPoint; - drawMarquee(); - }; - - var endMarquee = function() { - // marqueeBox start/end are opposite corners but we need - // topLeft and bottomRight. - var boxPoints = utils.boxPointsFromOppositeCorners(marqueeBox.start, marqueeBox.end); - var newViewport = utils.oppositeCornersFromBoxPoints(boxPoints); - - marqueeBox = {}; - drawMarquee(); - $scope.$emit('user:viewport:change:end', newViewport); - $scope.viewport = newViewport; - }; - - var startDrag = function($event) { - $scope.$emit('user:viewport:change:start'); - if (!$scope.mouseCoordinates) { - return; - } - $event.preventDefault(); - // Track drag location relative to position over element - // not domain, as chart viewport will change as we drag. - dragStart = $scope.mouseCoordinates.positionAsPlotPoint; - // Tell controller that we're starting to navigate. - return false; - }; - - var updateDrag = function() { - // calculate offset between points. Apply that offset to viewport. - var newPosition = $scope.mouseCoordinates.positionAsPlotPoint; - var dDomain = dragStart.domain - newPosition.domain; - var dRange = dragStart.range - newPosition.range; - - $scope.viewport = { - topLeft: { - domain: $scope.viewport.topLeft.domain + dDomain, - range: $scope.viewport.topLeft.range + dRange - }, - bottomRight: { - domain: $scope.viewport.bottomRight.domain + dDomain, - range: $scope.viewport.bottomRight.range + dRange - } - }; - }; - - var endDrag = function() { - dragStart = undefined; - $scope.$emit('user:viewport:change:end', $scope.viewport); - }; - - var watchForMarquee = function() { + function watchForMarquee() { $canvas.removeClass('plot-drag'); $canvas.addClass('plot-marquee'); $canvas.on('mousedown', startMarquee); $canvas.on('mouseup', endMarquee); $canvas.off('mousedown', startDrag); $canvas.off('mouseup', endDrag); - }; + } - var watchForDrag = function() { + function watchForDrag() { $canvas.addClass('plot-drag'); $canvas.removeClass('plot-marquee'); $canvas.on('mousedown', startDrag); $canvas.on('mouseup', endDrag); $canvas.off('mousedown', startMarquee); $canvas.off('mouseup', endMarquee); - }; + } - var stopWatching = function() { + function toggleInteractionMode(event) { + if (event.keyCode === '18') { // control key. + watchForDrag(); + } + } + + function resetInteractionMode(event) { + if (event.keyCode === '18') { + watchForMarquee(); + } + } + + function stopWatching() { $canvas.off('mousedown', startDrag); $canvas.off('mouseup', endDrag); $canvas.off('mousedown', startMarquee); $canvas.off('mouseup', endMarquee); window.removeEventListener('keydown', toggleInteractionMode); window.removeEventListener('keyup', resetInteractionMode); - }; - - var toggleInteractionMode = function(event) { - if (event.keyCode == '18') { // control key. - watchForDrag(); - } - }; - - var resetInteractionMode = function(event) { - if (event.keyCode == '18') { - watchForMarquee(); - } - }; + } $canvas.on('mousemove', trackMousePosition); $canvas.on('mouseleave', untrackMousePosition); @@ -226,7 +229,7 @@ define( window.addEventListener('keydown', toggleInteractionMode); window.addEventListener('keyup', resetInteractionMode); - var onViewportChange = function() { + function onViewportChange() { if ($scope.mouseCoordinates && chartElementBounds) { $scope.mouseCoordinates.positionAsPlotPoint = utils.elementPositionAsPlotPosition( @@ -235,11 +238,9 @@ define( $scope.viewport ); } - if (marqueeBox && marqueeBox.start) { - // TODO: Discuss whether marqueeBox start should be fixed to data or fixed to canvas element, especially when "isLive is true". - } + // TODO: Discuss whether marqueeBox start should be fixed to data or fixed to canvas element, especially when "isLive is true". updateAxesForCurrentViewport(); - }; + } $scope.$watchCollection('viewport', onViewportChange); diff --git a/platform/features/plot-reborn/src/draw/DrawLoader.js b/platform/features/plot-reborn/src/draw/DrawLoader.js index 3f517fe1c8..8389f67ca5 100644 --- a/platform/features/plot-reborn/src/draw/DrawLoader.js +++ b/platform/features/plot-reborn/src/draw/DrawLoader.js @@ -1,4 +1,4 @@ -/*global define */ +/*global define,$log */ define( [ @@ -25,7 +25,8 @@ define( the draw API to. */ getDrawAPI: function (canvas) { - for (var i = 0; i < CHARTS.length; i++) { + var i; + for (i = 0; i < CHARTS.length; i++) { try { return new CHARTS[i](canvas); } catch (e) { @@ -42,4 +43,4 @@ define( } }; } -); \ No newline at end of file +);