Merge remote-tracking branch 'upstream/plot-vee-two' into mobile_gestures_1

This commit is contained in:
Shivam Dave 2015-08-17 10:53:54 -07:00
commit 17e2da2d2c
16 changed files with 1532 additions and 1 deletions

View File

@ -14,7 +14,7 @@
"platform/features/imagery",
"platform/features/layout",
"platform/features/pages",
"platform/features/plot",
"platform/features/plot-reborn",
"platform/features/scrolling",
"platform/features/events",
"platform/forms",

View File

@ -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.

View File

@ -0,0 +1,96 @@
{
"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", "colorService"]
},
{
"key": "StackedPlotController",
"implementation": "controllers/StackedPlotController.js",
"depends": [ "$scope" ]
}
],
"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": []
}
],
"services": [
{
"key": "colorService",
"implementation": "services/ColorService.js",
"description": "Provides objects for working with colors."
}
]
}
}

View File

@ -0,0 +1,76 @@
<!--TODO: Don't require plotcontroller here. -->
<div class="gl-plot">
<div class="gl-plot-legend">
<span class="plot-legend-item"
ng-repeat="series in series track by $index">
<span class="plot-color-swatch"
ng-style="{ 'background-color': series.color.asHexString() }">
</span>
<span class="title-label">{{ series.name }}</span>
</span>
</div>
<div class="gl-plot-coords"
ng-if="mouseCoordinates">
{{ displayableDomain(mouseCoordinates.positionAsPlotPoint.domain) }},
{{ displayableRange(mouseCoordinates.positionAsPlotPoint.range) }}
</div>
<div class="gl-plot-axis-area gl-plot-y">
<div class="gl-plot-label gl-plot-y-label">
{{ axes.range.label}}
</div>
<div ng-repeat="tick in axes.range.ticks track by $index"
class="gl-plot-tick gl-plot-y-tick-label"
ng-style="{ top: (100 * $index / (axes.range.ticks.length - 1)) + '%' }"
style="margin-top: -0.50em;">
{{ displayableRange(tick) }}
</div>
</div>
<div class="gl-plot-display-area">
<div class="gl-plot-hash hash-v"
ng-repeat="tick in axes.domain.ticks track by $index"
ng-style="{ left: (100 * $index / (axes.domain.ticks.length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (axes.domain.ticks.length - 1)">
<!--TODO: Show/hide using CSS? -->
</div>
<div class="gl-plot-hash hash-h"
ng-repeat="tick in axes.range.ticks track by $index"
ng-style="{ bottom: (100 * $index / (axes.range.ticks.length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (axes.range.ticks.length - 1)">
<!--TODO: Show/hide using CSS? -->
</div>
<mct-chart series="series"
viewport="viewport"
rectangles="rectangles"
ng-mousemove="plot.trackMousePosition($event)"
ng-mouseleave="plot.untrackMousePosition()"
ng-mousedown="plot.startMarquee()"
ng-mouseup="plot.endMarquee()">
</mct-chart>
<span class="t-wait-spinner loading" ng-show="plot.isRequestPending()">
</span>
</div>
<div class="gl-plot-axis-area gl-plot-x">
<div ng-repeat="tick in axes.domain.ticks track by $index"
class="gl-plot-tick gl-plot-x-tick-label"
ng-show="$index > 0 && $index < (axes.domain.ticks.length - 1)"
ng-style="{ left: (100 * $index / (axes.domain.ticks.length - 1)) + '%' }">
{{ displayableDomain(tick) }}
</div>
<div class="gl-plot-label gl-plot-x-label">
{{ axes.domain.label }}
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
<span ng-controller="PlotController as controller">
<mct-plot series="series"
viewport="viewport"
rectangles="rectangles"
axes="axes"
displayable-range="displayableRange"
displayable-domain="displayableDomain">
</mct-plot>
</span>

View File

@ -0,0 +1,8 @@
<span ng-controller="StackedPlotController as stackedPlot">
<div class="gl-plot"
ng-style="{ height: 100 / telemetryObjects.length + '%'}"
ng-repeat="telemetryObject in telemetryObjects">
<mct-overlay-plot domain-object="telemetryObject"></mct-overlay-plot>
</div>
</span>

View File

@ -0,0 +1,185 @@
/*global define*/
define(
[],
function () {
"use strict";
// TODO: Store this in more accessible locations / retrieve from
// domainObject metadata.
var DOMAIN_INTERVAL = 2 * 60 * 1000; // Two minutes.
function PlotController($scope, colorService) {
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: {
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).toUTCString();
};
$scope.series = [];
$scope.rectangles = [];
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);
newTelemetry = {
domain: domainValue,
range: rangeValue
};
series.data.push(newTelemetry);
$scope.$broadcast('series:data:add', seriesIndex, [newTelemetry]);
}
function subscribeToDomainObject(domainObject) {
var telemetryCapability = domainObject.getCapability('telemetry'),
model = domainObject.getModel(),
series,
seriesIndex,
updater;
series = {
name: model.name,
// TODO: Bring back PlotPalette.
color: palette.getColor($scope.series.length),
data: []
};
$scope.series.push(series);
seriesIndex = $scope.series.indexOf(series);
updater = updateSeriesFromTelemetry.bind(
null,
series,
seriesIndex
);
subscriptions.push(telemetryCapability.subscribe(updater));
}
function unlinkDomainObject() {
subscriptions.forEach(function(subscription) {
subscription.unsubscribe();
});
subscriptions = [];
}
function linkDomainObject(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.');
}
}
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;
}
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.
// 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);
}
function viewportForMaxDomain() {
return {
topLeft: {
range: $scope.viewport.topLeft.range,
domain: maxDomain - DOMAIN_INTERVAL
},
bottomRight: {
range: $scope.viewport.bottomRight.range,
domain: maxDomain
}
};
}
function followDataIfLive() {
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);
return {
historyBack: function() {
// TODO: Step History Back.
},
historyForward: function() {
// TODO: Step History Forward.
},
resetZoom: function() {
// TODO: Reset view to defaults. Keep history stack alive?
}
};
}
return PlotController;
}
);

View File

@ -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;
}
);

View File

@ -0,0 +1,234 @@
/*global define,requestAnimationFrame,Float32Array*/
/**
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
*/
define(
["../draw/DrawLoader"],
function (DrawLoader) {
"use strict";
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
/**
* 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.
*
* @constructor
*/
function MCTChart($interval) {
function linkChart($scope, $element) {
var canvas = $element.find("canvas")[0],
isDestroyed = false,
activeInterval,
drawAPI,
lines = [],
offset;
drawAPI = DrawLoader.getDrawAPI(canvas);
if (!drawAPI) {
return;
}
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 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() {
// 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(
line.buffer,
line.color.asRGBAArray(),
line.pointCount
);
});
}
function drawRectangles() {
if ($scope.rectangles) {
$scope.rectangles.forEach(function(rect) {
drawAPI.drawSquare(
[
offset.domain(rect.start.domain),
offset.range(rect.start.range)
],
[
offset.domain(rect.end.domain),
offset.range(rect.end.range)
],
rect.color
);
});
}
}
function updateViewport() {
var dimensions,
origin;
dimensions = [
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)
)
];
origin = [
offset.domain(
$scope.viewport.topLeft.domain
),
offset.range(
$scope.viewport.bottomRight.range
)
];
drawAPI.setDimensions(
dimensions,
origin
);
}
function onSeriesDataAdd(event, seriesIndex, points) {
var line = lines[seriesIndex];
points.forEach(function (point) {
line.buffer[2*line.pointCount] = offset.domain(point.domain);
line.buffer[2*line.pointCount+1] = offset.range(point.range);
line.pointCount += 1;
});
}
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);
$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;
}
);

View File

@ -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: "="
}
};
};
}
);

View File

@ -0,0 +1,268 @@
/*global define,window*/
define(
[
'../lib/utils'
],
function (utils) {
"use strict";
var RANGE_TICK_COUNT = 7,
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.
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,
marqueeBox = {},
marqueeRect, // Set when exists.
chartElementBounds,
$canvas = $element.find('canvas');
function updateAxesForCurrentViewport() {
// Update axes definitions for current viewport.
['domain', 'range'].forEach(function(axisName) {
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 (tickNumber = 0; tickNumber < axis.tickCount; tickNumber++) {
tickIncrement = (axisSize * (tickNumber / denominator));
tickValue = firstTick - tickIncrement;
axis.ticks.push(
tickValue
);
}
});
}
function drawMarquee() {
// 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');
}
}
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);
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(),
positionOverElement,
positionAsPlotPoint;
chartElementBounds = bounds;
positionOverElement = {
x: $event.clientX - bounds.left,
y: $event.clientY - bounds.top
};
positionAsPlotPoint = utils.elementPositionAsPlotPosition(
positionOverElement,
bounds,
$scope.viewport
);
$scope.mouseCoordinates = {
positionOverElement: positionOverElement,
positionAsPlotPoint: positionAsPlotPoint
};
if (marqueeBox && marqueeBox.start) {
updateMarquee();
}
if (dragStart) {
updateDrag();
}
}
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);
}
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);
}
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);
}
$canvas.on('mousemove', trackMousePosition);
$canvas.on('mouseleave', untrackMousePosition);
watchForMarquee();
window.addEventListener('keydown', toggleInteractionMode);
window.addEventListener('keyup', resetInteractionMode);
function onViewportChange() {
if ($scope.mouseCoordinates && chartElementBounds) {
$scope.mouseCoordinates.positionAsPlotPoint =
utils.elementPositionAsPlotPosition(
$scope.mouseCoordinates.positionOverElement,
chartElementBounds,
$scope.viewport
);
}
// 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;
}
);

View File

@ -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;
}
);

View File

@ -0,0 +1,46 @@
/*global define,$log */
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) {
var i;
for (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;
}
};
}
);

View File

@ -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;
}
);

View File

@ -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;
});

View File

@ -0,0 +1,154 @@
/*****************************************************************************
* 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 stores a set of colors and allows for different
* methods of color allocation.
*
* @constructor
*/
function ColorPalette() {
this.nextColor = 0;
this.colors = COLOR_PALETTE.map(function (color) {
return new Color(color);
});
}
/**
* @returns {Color} the next unused color in the palette. If all colors
* have been allocated, it will wrap around.
*/
ColorPalette.prototype.getNextColor = function () {
var color = this.colors[this.nextColor % this.colors.length];
this.nextColor++;
return color;
};
/**
* @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;
}
);