mirror of
https://github.com/nasa/openmct.git
synced 2025-01-18 18:57:01 +00:00
Merge remote-tracking branch 'origin/wtd533' into open-master
Conflicts: bundles.json
This commit is contained in:
commit
301153cc1b
@ -7,6 +7,6 @@
|
||||
"platform/commonUI/dialog",
|
||||
"platform/commonUI/general",
|
||||
"platform/telemetry",
|
||||
|
||||
|
||||
"example/persistence"
|
||||
]
|
40
example/generator/bundle.json
Normal file
40
example/generator/bundle.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Sine Wave Generator",
|
||||
"description": "Example of a component that produces dataa.",
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"implementation": "SinewaveTelemetryProvider.js",
|
||||
"type": "provider",
|
||||
"provides": "telemetryService",
|
||||
"depends": [ "$q", "$timeout" ]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"key": "generator",
|
||||
"name": "Sine Wave Generator",
|
||||
"glyph": "T",
|
||||
"description": "A sine wave generator",
|
||||
"model": {
|
||||
"telemetry": {
|
||||
"period": 10
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"source": "generator"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"label": "Period",
|
||||
"control": "_textfield",
|
||||
"key": "period",
|
||||
"required": true,
|
||||
"property": [ "telemetry", "period" ],
|
||||
"pattern": "^\\d*(\\.\\d*)?$"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
42
example/generator/src/SinewaveTelemetry.js
Normal file
42
example/generator/src/SinewaveTelemetry.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*global define,Promise*/
|
||||
|
||||
/**
|
||||
* Module defining SinewaveTelemetry. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
var firstObservedTime = Date.now();
|
||||
|
||||
/**
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SinewaveTelemetry(request) {
|
||||
var latestObservedTime = Date.now(),
|
||||
count = Math.floor((latestObservedTime - firstObservedTime) / 1000),
|
||||
period = request.period || 30,
|
||||
generatorData = {};
|
||||
|
||||
generatorData.getPointCount = function () {
|
||||
return count;
|
||||
};
|
||||
|
||||
generatorData.getDomainValue = function (i, domain) {
|
||||
return i * 1000 +
|
||||
(domain !== 'delta' ? firstObservedTime : 0);
|
||||
};
|
||||
|
||||
generatorData.getRangeValue = function (i, range) {
|
||||
range = range || "sin";
|
||||
return Math[range](i * Math.PI * 2 / period);
|
||||
};
|
||||
|
||||
return generatorData;
|
||||
}
|
||||
|
||||
return SinewaveTelemetry;
|
||||
}
|
||||
);
|
53
example/generator/src/SinewaveTelemetryProvider.js
Normal file
53
example/generator/src/SinewaveTelemetryProvider.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*global define,Promise*/
|
||||
|
||||
/**
|
||||
* Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
["./SinewaveTelemetry"],
|
||||
function (SinewaveTelemetry) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SinewaveTelemetryProvider($q, $timeout) {
|
||||
|
||||
//
|
||||
function matchesSource(request) {
|
||||
return request.source === "generator";
|
||||
}
|
||||
|
||||
// Used internally; this will be repacked by doPackage
|
||||
function generateData(request) {
|
||||
return {
|
||||
key: request.key,
|
||||
telemetry: new SinewaveTelemetry(request)
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
function doPackage(results) {
|
||||
var packaged = {};
|
||||
results.forEach(function (result) {
|
||||
packaged[result.key] = result.telemetry;
|
||||
});
|
||||
// Format as expected (sources -> keys -> telemetry)
|
||||
return { generator: packaged };
|
||||
}
|
||||
|
||||
function requestTelemetry(requests) {
|
||||
return $timeout(function () {
|
||||
return doPackage(requests.filter(matchesSource).map(generateData));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return {
|
||||
requestTelemetry: requestTelemetry
|
||||
};
|
||||
}
|
||||
|
||||
return SinewaveTelemetryProvider;
|
||||
}
|
||||
);
|
27
platform/features/plot/bundle.json
Normal file
27
platform/features/plot/bundle.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Plot view for telemetry",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"name": "Plot",
|
||||
"templateUrl": "templates/plot.html",
|
||||
"needs": [ "telemetry" ],
|
||||
"delegation": true
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctChart",
|
||||
"implementation": "MCTChart.js",
|
||||
"depends": [ "$interval", "$log" ]
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "PlotController",
|
||||
"implementation": "PlotController.js",
|
||||
"depends": [ "$scope" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
6
platform/features/plot/lib/moment.min.js
vendored
Normal file
6
platform/features/plot/lib/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
121
platform/features/plot/res/templates/plot.html
Normal file
121
platform/features/plot/res/templates/plot.html
Normal file
@ -0,0 +1,121 @@
|
||||
<div class="gl-plot"
|
||||
ng-controller="TelemetryController as telemetry">
|
||||
<span ng-controller="PlotController as plot"
|
||||
ng-mouseleave="representation.showControls = false">
|
||||
|
||||
<div class="gl-plot-legend">
|
||||
<span class='plot-legend-item'
|
||||
ng-repeat="telemetryObject in telemetry.getTelemetryObjects() track by $index">
|
||||
<span class='plot-color-swatch' ng-style="{ 'background-color': plot.getColor($index) }"></span>
|
||||
<span class='title-label'>{{telemetryObject.getModel().name}}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gl-plot-coords"
|
||||
ng-if="representation.showControls">
|
||||
{{plot.getHoverCoordinates().join(', ')}}
|
||||
</div>
|
||||
|
||||
<div class="gl-plot-axis-area gl-plot-y">
|
||||
|
||||
<div class="gl-plot-label gl-plot-y-label"
|
||||
ng-show="!representation.showControls">
|
||||
{{axes[1].active.name}}
|
||||
</div>
|
||||
|
||||
<div ng-repeat="tick in rangeTicks"
|
||||
class="gl-plot-tick gl-plot-y-tick-label"
|
||||
ng-style="{ bottom: (100 * $index / (rangeTicks.length - 1)) + '%' }">
|
||||
{{tick.label}}
|
||||
</div>
|
||||
|
||||
<div class="gl-plot-y-options gl-plot-local-controls"
|
||||
ng-show="representation.showControls"
|
||||
ng-if="axes[1].options.length > 0">
|
||||
<div class='form-control shell select'>
|
||||
<select class="form-control input shell select"
|
||||
ng-model="axes[1].active"
|
||||
ng-options="option.name for option in axes[1].options">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gl-plot-display-area"
|
||||
ng-mouseenter="representation.showControls = true">
|
||||
|
||||
|
||||
<div class="gl-plot-hash hash-v"
|
||||
ng-repeat="tick in domainTicks"
|
||||
ng-style="{ left: (100 * $index / (domainTicks.length - 1)) + '%', height: '100%' }"
|
||||
ng-show="$index > 0 && $index < (domainTicks.length - 1)">
|
||||
</div>
|
||||
<div class="gl-plot-hash hash-h"
|
||||
ng-repeat="tick in rangeTicks"
|
||||
ng-style="{ bottom: (100 * $index / (rangeTicks.length - 1)) + '%', width: '100%' }"
|
||||
ng-show="$index > 0 && $index < (rangeTicks.length - 1)">
|
||||
</div>
|
||||
|
||||
<mct-chart draw="draw"
|
||||
ng-mousemove="plot.hover($event)"
|
||||
ng-mousedown="plot.startMarquee($event)"
|
||||
ng-mouseup="plot.endMarquee($event)">
|
||||
</mct-chart>
|
||||
|
||||
<!-- TODO: Move into correct position; make part of group; infer from set of actions -->
|
||||
<div class="gl-plot-local-controls"
|
||||
style="position: absolute; top: 8px; right: 8px;">
|
||||
|
||||
<a href=""
|
||||
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
|
||||
ng-click="plot.stepBackPanZoom()"
|
||||
ng-show="plot.isZoomed()"
|
||||
title="Restore previous pan/zoom">
|
||||
<span class="ui-symbol icon"><</span>
|
||||
</a>
|
||||
|
||||
<a href=""
|
||||
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
|
||||
ng-click="plot.unzoom()"
|
||||
ng-show="plot.isZoomed()"
|
||||
title="Reset pan/zoom">
|
||||
<span class="ui-symbol icon">I</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<span class="t-wait-spinner loading" ng-show="telemetry.isRequestPending()">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gl-plot-axis-area gl-plot-x">
|
||||
|
||||
<div ng-repeat="tick in domainTicks"
|
||||
class="gl-plot-tick gl-plot-x-tick-label"
|
||||
ng-show="$index > 0 && $index < (domainTicks.length - 1)"
|
||||
ng-style="{ left: (100 * $index / (domainTicks.length - 1)) + '%' }">
|
||||
{{tick.label}}
|
||||
</div>
|
||||
|
||||
<div class="gl-plot-label gl-plot-x-label">
|
||||
{{axes[0].active.name}}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gl-plot-x-options gl-plot-local-controls"
|
||||
ng-show="representation.showControls"
|
||||
ng-if="axes[0].options.length > 0">
|
||||
<div class='form-control shell select'>
|
||||
<select class="form-control input shell select"
|
||||
ng-model="axes[0].active"
|
||||
ng-options="option.name for option in axes[0].options">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
144
platform/features/plot/src/GLChart.js
Normal file
144
platform/features/plot/src/GLChart.js
Normal file
@ -0,0 +1,144 @@
|
||||
/*global define,Float32Array*/
|
||||
|
||||
/**
|
||||
* Module defining GLPlot. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
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 new chart which uses WebGL for rendering.
|
||||
*
|
||||
* @constructor
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if WebGL is unavailable.
|
||||
*/
|
||||
function GLChart(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 () {
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
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) {
|
||||
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 GLChart;
|
||||
}
|
||||
);
|
138
platform/features/plot/src/MCTChart.js
Normal file
138
platform/features/plot/src/MCTChart.js
Normal file
@ -0,0 +1,138 @@
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
["./GLChart"],
|
||||
function (GLChart) {
|
||||
"use strict";
|
||||
|
||||
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
|
||||
|
||||
/**
|
||||
* The mct-chart directive provides a canvas element which can be
|
||||
* drawn upon, to support Plot view and similar visualizations.
|
||||
*
|
||||
* This directive takes one attribute, "draw", which is an Angular
|
||||
* expression which will be two-way bound to a drawing object. This
|
||||
* drawing object should contain:
|
||||
*
|
||||
* * `dimensions`: An object describing the logical bounds of the
|
||||
* drawable area, containing two fields:
|
||||
* * `origin`: The position, in logical coordinates, of the
|
||||
* lower-left corner of the chart area. A two-element array.
|
||||
* * `dimensions`: A two-element array containing the width
|
||||
* and height of the chart area, in logical coordinates.
|
||||
* * `lines`: An array of lines to be drawn, where each line is
|
||||
* expressed as an object containing:
|
||||
* * `buffer`: A Float32Array containing points in the line,
|
||||
* in logical coordinate, in sequential x/y pairs.
|
||||
* * `color`: The color of the line, as a four-element RGBA
|
||||
* array, where each element is in the range of 0.0-1.0
|
||||
* * `points`: The number of points in the line.
|
||||
* * `boxes`: An array of rectangles to draw in the chart area
|
||||
* (used for marquee zoom). Each is an object containing:
|
||||
* * `start`: The first corner of the rectangle (as a two-element
|
||||
* array, logical coordinates)
|
||||
* * `end`: The opposite corner of the rectangle (again, as a
|
||||
* two-element array)
|
||||
* * `color`: The color of the box, as a four-element RGBA
|
||||
* array, where each element is in the range of 0.0-1.0
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MCTChart($interval, $log) {
|
||||
|
||||
function linkChart(scope, element) {
|
||||
var canvas = element.find("canvas")[0],
|
||||
chart;
|
||||
|
||||
// Try to initialize GLChart, which allows drawing using WebGL.
|
||||
// This may fail, particularly where browsers do not support
|
||||
// WebGL, so catch that here.
|
||||
try {
|
||||
chart = new GLChart(canvas);
|
||||
} catch (e) {
|
||||
$log.warn("Cannot initialize mct-chart; " + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle drawing, based on contents of the "draw" object
|
||||
// in scope
|
||||
function doDraw(draw) {
|
||||
// Ensure canvas context has same resolution
|
||||
// as canvas element
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
// Clear previous contents
|
||||
chart.clear();
|
||||
|
||||
// Nothing to draw if no draw object defined
|
||||
if (!draw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set logical boundaries for the chart
|
||||
chart.setDimensions(
|
||||
draw.dimensions || [1, 1],
|
||||
draw.origin || [0, 0]
|
||||
);
|
||||
|
||||
// Draw line segments
|
||||
(draw.lines || []).forEach(function (line) {
|
||||
chart.drawLine(
|
||||
line.buffer,
|
||||
line.color,
|
||||
line.points
|
||||
);
|
||||
});
|
||||
|
||||
// Draw boxes (e.g. marquee zoom rect)
|
||||
(draw.boxes || []).forEach(function (box) {
|
||||
chart.drawSquare(
|
||||
box.start,
|
||||
box.end,
|
||||
box.color
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Issue a drawing call, if-and-only-if canvas size
|
||||
// has changed. This will be called on a timer, since
|
||||
// there is no event to depend on.
|
||||
function drawIfResized() {
|
||||
if (canvas.width !== canvas.offsetWidth ||
|
||||
canvas.height !== canvas.offsetHeight) {
|
||||
doDraw(scope.draw);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for resize, on a timer
|
||||
$interval(drawIfResized, 1000);
|
||||
|
||||
// Watch "draw" for external changes to the set of
|
||||
// things to be drawn.
|
||||
scope.$watchCollection("draw", doDraw);
|
||||
}
|
||||
|
||||
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: "=" }
|
||||
};
|
||||
}
|
||||
|
||||
return MCTChart;
|
||||
}
|
||||
);
|
307
platform/features/plot/src/PlotController.js
Normal file
307
platform/features/plot/src/PlotController.js
Normal file
@ -0,0 +1,307 @@
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining PlotController. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
[
|
||||
"./elements/PlotPreparer",
|
||||
"./elements/PlotPalette",
|
||||
"./elements/PlotPanZoomStack",
|
||||
"./elements/PlotPosition",
|
||||
"./elements/PlotTickGenerator",
|
||||
"./elements/PlotFormatter",
|
||||
"./elements/PlotAxis"
|
||||
],
|
||||
function (
|
||||
PlotPreparer,
|
||||
PlotPalette,
|
||||
PlotPanZoomStack,
|
||||
PlotPosition,
|
||||
PlotTickGenerator,
|
||||
PlotFormatter,
|
||||
PlotAxis
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var AXIS_DEFAULTS = [
|
||||
{ "name": "Time" },
|
||||
{ "name": "Value" }
|
||||
],
|
||||
DOMAIN_TICKS = 5,
|
||||
RANGE_TICKS = 7;
|
||||
|
||||
/**
|
||||
* The PlotController is responsible for any computation/logic
|
||||
* associated with displaying the plot view. Specifically, these
|
||||
* responsibilities include:
|
||||
*
|
||||
* * Describing axes and labeling.
|
||||
* * Handling user interactions.
|
||||
* * Deciding what needs to be drawn in the chart area.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function PlotController($scope) {
|
||||
var mousePosition,
|
||||
marqueeStart,
|
||||
panZoomStack = new PlotPanZoomStack([], []),
|
||||
formatter = new PlotFormatter(),
|
||||
domainOffset;
|
||||
|
||||
// Utility, for map/forEach loops. Index 0 is domain,
|
||||
// index 1 is range.
|
||||
function formatValue(v, i) {
|
||||
return (i ?
|
||||
formatter.formatRangeValue :
|
||||
formatter.formatDomainValue)(v);
|
||||
}
|
||||
|
||||
// Converts from pixel coordinates to domain-range,
|
||||
// to interpret mouse gestures.
|
||||
function mousePositionToDomainRange(mousePosition) {
|
||||
return new PlotPosition(
|
||||
mousePosition.x,
|
||||
mousePosition.y,
|
||||
mousePosition.width,
|
||||
mousePosition.height,
|
||||
panZoomStack
|
||||
).getPosition();
|
||||
}
|
||||
|
||||
// Utility function to get the mouse position (in x,y
|
||||
// pixel coordinates in the canvas area) from a mouse
|
||||
// event object.
|
||||
function toMousePosition($event) {
|
||||
var target = $event.target,
|
||||
bounds = target.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: $event.clientX - bounds.left,
|
||||
y: $event.clientY - bounds.top,
|
||||
width: bounds.width,
|
||||
height: bounds.height
|
||||
};
|
||||
}
|
||||
|
||||
// Convert a domain-range position to a displayable
|
||||
// position. This will subtract the domain offset, which
|
||||
// is used to bias domain values to minimize loss-of-precision
|
||||
// associated with conversion to a 32-bit floating point
|
||||
// format (which is needed in the chart area itself, by WebGL.)
|
||||
function toDisplayable(position) {
|
||||
return [ position[0] - domainOffset, position[1] ];
|
||||
}
|
||||
|
||||
// Update the drawable marquee area to reflect current
|
||||
// mouse position (or don't show it at all, if no marquee
|
||||
// zoom is in progress)
|
||||
function updateMarqueeBox() {
|
||||
// Express this as a box in the draw object, which
|
||||
// is passed to an mct-chart in the template for rendering.
|
||||
$scope.draw.boxes = marqueeStart ?
|
||||
[{
|
||||
start: toDisplayable(mousePositionToDomainRange(marqueeStart)),
|
||||
end: toDisplayable(mousePositionToDomainRange(mousePosition)),
|
||||
color: [1, 1, 1, 0.5 ]
|
||||
}] : undefined;
|
||||
}
|
||||
|
||||
// Update the bounds (origin and dimensions) of the drawing area.
|
||||
function updateDrawingBounds() {
|
||||
var panZoom = panZoomStack.getPanZoom();
|
||||
|
||||
// Communicate pan-zoom state from stack to the draw object
|
||||
// which is passed to mct-chart in the template.
|
||||
$scope.draw.dimensions = panZoom.dimensions;
|
||||
$scope.draw.origin = [
|
||||
panZoom.origin[0] - domainOffset,
|
||||
panZoom.origin[1]
|
||||
];
|
||||
}
|
||||
|
||||
// Update tick marks in scope.
|
||||
function updateTicks() {
|
||||
var tickGenerator = new PlotTickGenerator(panZoomStack, formatter);
|
||||
|
||||
$scope.domainTicks =
|
||||
tickGenerator.generateDomainTicks(DOMAIN_TICKS);
|
||||
$scope.rangeTicks =
|
||||
tickGenerator.generateRangeTicks(RANGE_TICKS);
|
||||
}
|
||||
|
||||
// Populate the scope with axis information (specifically, options
|
||||
// available for each axis.)
|
||||
function setupAxes(metadatas) {
|
||||
$scope.axes = [
|
||||
new PlotAxis("domain", metadatas, AXIS_DEFAULTS[0]),
|
||||
new PlotAxis("range", metadatas, AXIS_DEFAULTS[1])
|
||||
];
|
||||
}
|
||||
|
||||
// Respond to newly-available telemetry data; update the
|
||||
// drawing area accordingly.
|
||||
function plotTelemetry() {
|
||||
var prepared, datas, telemetry;
|
||||
|
||||
// Get a reference to the TelemetryController
|
||||
telemetry = $scope.telemetry;
|
||||
|
||||
// Nothing to plot without TelemetryController
|
||||
if (!telemetry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure axes have been initialized (we will want to
|
||||
// get the active axis below)
|
||||
if (!$scope.axes) {
|
||||
setupAxes(telemetry.getMetadata());
|
||||
}
|
||||
|
||||
// Get data sets
|
||||
datas = telemetry.getResponse();
|
||||
|
||||
// Prepare data sets for rendering
|
||||
prepared = new PlotPreparer(
|
||||
datas,
|
||||
($scope.axes[0].active || {}).key,
|
||||
($scope.axes[1].active || {}).key
|
||||
);
|
||||
|
||||
// Fit to the boundaries of the data, but don't
|
||||
// override any user-initiated pan-zoom changes.
|
||||
panZoomStack.setBasePanZoom(
|
||||
prepared.getOrigin(),
|
||||
prepared.getDimensions()
|
||||
);
|
||||
|
||||
// Track the domain offset, used to bias domain values
|
||||
// to minimize loss of precision when converted to 32-bit
|
||||
// floating point values for display.
|
||||
domainOffset = prepared.getDomainOffset();
|
||||
|
||||
// Draw the buffers. Select color by index.
|
||||
$scope.draw.lines = prepared.getBuffers().map(function (buf, i) {
|
||||
return {
|
||||
buffer: buf,
|
||||
color: PlotPalette.getFloatColor(i),
|
||||
points: buf.length / 2
|
||||
};
|
||||
});
|
||||
|
||||
updateDrawingBounds();
|
||||
updateMarqueeBox();
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
// Perform a marquee zoom.
|
||||
function marqueeZoom(start, end) {
|
||||
// Determine what boundary is described by the marquee,
|
||||
// in domain-range values. Use the minima for origin, so that
|
||||
// it doesn't matter what direction the user marqueed in.
|
||||
var a = mousePositionToDomainRange(start),
|
||||
b = mousePositionToDomainRange(end),
|
||||
origin = [
|
||||
Math.min(a[0], b[0]),
|
||||
Math.min(a[1], b[1])
|
||||
],
|
||||
dimensions = [
|
||||
Math.max(a[0], b[0]) - origin[0],
|
||||
Math.max(a[1], b[1]) - origin[1]
|
||||
];
|
||||
|
||||
// Push the new state onto the pan-zoom stack
|
||||
panZoomStack.pushPanZoom(origin, dimensions);
|
||||
|
||||
// Make sure tick marks reflect new bounds
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
$scope.$watch("telemetry.getMetadata()", setupAxes);
|
||||
$scope.$on("telemetryUpdate", plotTelemetry);
|
||||
$scope.draw = {};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the color (as a style-friendly string) to use
|
||||
* for plotting the trace at the specified index.
|
||||
* @param {number} index the index of the trace
|
||||
* @returns {string} the color, in #RRGGBB form
|
||||
*/
|
||||
getColor: function (index) {
|
||||
return PlotPalette.getStringColor(index);
|
||||
},
|
||||
/**
|
||||
* Get the coordinates (as displayable text) for the
|
||||
* current mouse position.
|
||||
* @returns {string[]} the displayable domain and range
|
||||
* coordinates over which the mouse is hovered
|
||||
*/
|
||||
getHoverCoordinates: function () {
|
||||
return mousePosition ?
|
||||
mousePositionToDomainRange(
|
||||
mousePosition
|
||||
).map(formatValue) : [];
|
||||
},
|
||||
/**
|
||||
* Handle mouse movement over the chart area.
|
||||
* @param $event the mouse event
|
||||
*/
|
||||
hover: function ($event) {
|
||||
mousePosition = toMousePosition($event);
|
||||
if (marqueeStart) {
|
||||
updateMarqueeBox();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initiate a marquee zoom action.
|
||||
* @param $event the mouse event
|
||||
*/
|
||||
startMarquee: function ($event) {
|
||||
mousePosition = marqueeStart = toMousePosition($event);
|
||||
updateMarqueeBox();
|
||||
},
|
||||
/**
|
||||
* Complete a marquee zoom action.
|
||||
* @param $event the mouse event
|
||||
*/
|
||||
endMarquee: function ($event) {
|
||||
mousePosition = toMousePosition($event);
|
||||
if (marqueeStart) {
|
||||
marqueeZoom(marqueeStart, mousePosition);
|
||||
marqueeStart = undefined;
|
||||
updateMarqueeBox();
|
||||
updateDrawingBounds();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Check if the plot is zoomed or panned out
|
||||
* of its default state (to determine whether back/unzoom
|
||||
* controls should be shown)
|
||||
* @returns {boolean} true if not in default state
|
||||
*/
|
||||
isZoomed: function () {
|
||||
return panZoomStack.getDepth() > 1;
|
||||
},
|
||||
/**
|
||||
* Undo the most recent pan/zoom change and restore
|
||||
* the prior state.
|
||||
*/
|
||||
stepBackPanZoom: function () {
|
||||
panZoomStack.popPanZoom();
|
||||
updateDrawingBounds();
|
||||
},
|
||||
/**
|
||||
* Undo all pan/zoom changes and restore the initial state.
|
||||
*/
|
||||
unzoom: function () {
|
||||
panZoomStack.clearPanZoom();
|
||||
updateDrawingBounds();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
return PlotController;
|
||||
}
|
||||
);
|
66
platform/features/plot/src/elements/PlotAxis.js
Normal file
66
platform/features/plot/src/elements/PlotAxis.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A PlotAxis provides a template-ready set of options
|
||||
* for the domain or range axis, sufficient to populate
|
||||
* selectors.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} axisType the field in metadatas to
|
||||
* look at for axis options; usually one of
|
||||
* "domains" or "ranges"
|
||||
* @param {object[]} metadatas metadata objects, as
|
||||
* returned by the `getMetadata()` method of
|
||||
* a `telemetry` capability.
|
||||
* @param {object} defaultValue the value to use for the
|
||||
* active state in the event that no options are
|
||||
* found; should contain "name" and "key" at
|
||||
* minimum.
|
||||
*
|
||||
*/
|
||||
function PlotAxis(axisType, metadatas, defaultValue) {
|
||||
var keys = {},
|
||||
options = [];
|
||||
|
||||
// Look through all metadata objects and assemble a list
|
||||
// of all possible domain or range options
|
||||
function buildOptionsForMetadata(m) {
|
||||
(m[axisType] || []).forEach(function (option) {
|
||||
if (!keys[option.key]) {
|
||||
keys[option.key] = true;
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(metadatas || []).forEach(buildOptionsForMetadata);
|
||||
|
||||
// Plot axis will be used directly from the Angular
|
||||
// template, so expose properties directly to facilitate
|
||||
// two-way data binding (for drop-down menus)
|
||||
return {
|
||||
/**
|
||||
* The set of options applicable for this axis;
|
||||
* an array of objects, where each object contains a
|
||||
* "key" field and a "name" field (for machine- and
|
||||
* human-readable names respectively)
|
||||
*/
|
||||
options: options,
|
||||
/**
|
||||
* The currently chosen option for this axis. An
|
||||
* initial value is provided; this will be updated
|
||||
* directly form the plot template.
|
||||
*/
|
||||
active: options[0] || defaultValue
|
||||
};
|
||||
}
|
||||
|
||||
return PlotAxis;
|
||||
|
||||
}
|
||||
);
|
48
platform/features/plot/src/elements/PlotFormatter.js
Normal file
48
platform/features/plot/src/elements/PlotFormatter.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*global define,moment*/
|
||||
|
||||
define(
|
||||
["../../lib/moment.min"],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// Date format to use for domain values; in particular,
|
||||
// use day-of-year instead of month/day
|
||||
var DATE_FORMAT = "YYYY-DDD HH:mm:ss";
|
||||
|
||||
/**
|
||||
* The PlotFormatter is responsible for formatting (as text
|
||||
* for display) values along either the domain or range of a
|
||||
* plot.
|
||||
*/
|
||||
function PlotFormatter() {
|
||||
function formatDomainValue(v) {
|
||||
return moment.utc(v).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
function formatRangeValue(v) {
|
||||
return v.toFixed(1);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Format a domain value.
|
||||
* @param {number} v the domain value; a timestamp
|
||||
* in milliseconds since start of 1970
|
||||
* @returns {string} a textual representation of the
|
||||
* data and time, suitable for display.
|
||||
*/
|
||||
formatDomainValue: formatDomainValue,
|
||||
/**
|
||||
* Format a range value.
|
||||
* @param {number} v the range value; a numeric value
|
||||
* @returns {string} a textual representation of the
|
||||
* value, suitable for display.
|
||||
*/
|
||||
formatRangeValue: formatRangeValue
|
||||
};
|
||||
}
|
||||
|
||||
return PlotFormatter;
|
||||
|
||||
}
|
||||
);
|
113
platform/features/plot/src/elements/PlotPalette.js
Normal file
113
platform/features/plot/src/elements/PlotPalette.js
Normal file
@ -0,0 +1,113 @@
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Plot palette. Defines colors for various plot lines.
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
// Prepare different forms of the palette, since we wish to
|
||||
// describe colors in several ways (as RGB 0-255, as
|
||||
// RGB 0.0-1.0, or as stylesheet-appropriate #-prefixed colors).
|
||||
var integerPalette = [
|
||||
[ 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 ]
|
||||
], stringPalette = integerPalette.map(function (arr) {
|
||||
// Convert to # notation for use in styles
|
||||
return '#' + arr.map(function (c) {
|
||||
return (c < 16 ? '0' : '') + c.toString(16);
|
||||
}).join('');
|
||||
}), floatPalette = integerPalette.map(function (arr) {
|
||||
return arr.map(function (c) {
|
||||
return c / 255.0;
|
||||
}).concat([1]); // RGBA
|
||||
});
|
||||
|
||||
/**
|
||||
* PlotPalette allows a consistent set of colors to be retrieved
|
||||
* by index, in various color formats. All PlotPalette methods are
|
||||
* static, so there is no need for a constructor call; using
|
||||
* this will simply return PlotPalette itself.
|
||||
* @constructor
|
||||
*/
|
||||
function PlotPalette() {
|
||||
return PlotPalette;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a color in the plot's palette, by index.
|
||||
* This will be returned as a three element array of RGB
|
||||
* values, as integers in the range of 0-255.
|
||||
* @param {number} i the index of the color to look up
|
||||
* @return {number[]} the color, as integer RGB values
|
||||
*/
|
||||
PlotPalette.getIntegerColor = function (i) {
|
||||
return integerPalette[Math.floor(i) % integerPalette.length];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Look up a color in the plot's palette, by index.
|
||||
* This will be returned as a three element array of RGB
|
||||
* values, in the range of 0.0-1.0.
|
||||
*
|
||||
* This format is present specifically to support use with
|
||||
* WebGL, which expects colors of that form.
|
||||
*
|
||||
* @param {number} i the index of the color to look up
|
||||
* @return {number[]} the color, as floating-point RGB values
|
||||
*/
|
||||
PlotPalette.getFloatColor = function (i) {
|
||||
return floatPalette[Math.floor(i) % floatPalette.length];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Look up a color in the plot's palette, by index.
|
||||
* This will be returned as a string using #-prefixed
|
||||
* six-digit RGB hex notation (e.g. #FF0000)
|
||||
* See http://www.w3.org/TR/css3-color/#rgb-color.
|
||||
*
|
||||
* This format is useful for representing colors in in-line
|
||||
* styles.
|
||||
*
|
||||
* @param {number} i the index of the color to look up
|
||||
* @return {string} the color, as a style-friendly string
|
||||
*/
|
||||
PlotPalette.getStringColor = function (i) {
|
||||
return stringPalette[Math.floor(i) % stringPalette.length];
|
||||
};
|
||||
|
||||
return PlotPalette;
|
||||
|
||||
}
|
||||
);
|
137
platform/features/plot/src/elements/PlotPanZoomStack.js
Normal file
137
platform/features/plot/src/elements/PlotPanZoomStack.js
Normal file
@ -0,0 +1,137 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The PlotPanZoomStack is responsible for maintaining the
|
||||
* pan-zoom state of a plot (expressed as a boundary starting
|
||||
* at an origin and extending to certain dimensions) in a
|
||||
* stack, to support the back and unzoom buttons in plot controls.
|
||||
*
|
||||
* Dimensions and origins are here described each by two-element
|
||||
* arrays, where the first element describes a value or quantity
|
||||
* along the domain axis, and the second element describes the same
|
||||
* along the range axis.
|
||||
*
|
||||
* @constructor
|
||||
* @param {number[]} origin the plot's origin, initially
|
||||
* @param {number[]} dimensions the plot's dimensions, initially
|
||||
*/
|
||||
function PlotPanZoomStack(origin, dimensions) {
|
||||
// Use constructor parameters as the stack's initial state
|
||||
var stack = [{ origin: origin, dimensions: dimensions }];
|
||||
|
||||
// Various functions which follow are simply wrappers for
|
||||
// normal stack-like array methods, with the exception that
|
||||
// they prevent undesired modification and enforce that this
|
||||
// stack must remain non-empty.
|
||||
// See JSDoc for specific methods below for more detail.
|
||||
function getDepth() {
|
||||
return stack.length;
|
||||
}
|
||||
|
||||
function pushPanZoom(origin, dimensions) {
|
||||
stack.push({ origin: origin, dimensions: dimensions });
|
||||
}
|
||||
|
||||
function popPanZoom() {
|
||||
if (stack.length > 1) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function clearPanZoom() {
|
||||
stack = [stack[0]];
|
||||
}
|
||||
|
||||
function setBasePanZoom(origin, dimensions) {
|
||||
stack[0] = { origin: origin, dimensions: dimensions };
|
||||
}
|
||||
|
||||
function getPanZoom() {
|
||||
return stack[stack.length - 1];
|
||||
}
|
||||
|
||||
function getOrigin() {
|
||||
return getPanZoom().origin;
|
||||
}
|
||||
|
||||
function getDimensions() {
|
||||
return getPanZoom().dimensions;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the current stack depth; that is, the number
|
||||
* of items on the stack. A depth of one means that no
|
||||
* panning or zooming relative to the base value has
|
||||
* been applied.
|
||||
* @returns {number} the depth of the stack
|
||||
*/
|
||||
getDepth: getDepth,
|
||||
|
||||
/**
|
||||
* Push a new pan-zoom state onto the stack; this will
|
||||
* become the active pan-zoom state.
|
||||
* @param {number[]} origin the new origin
|
||||
* @param {number[]} dimensions the new dimensions
|
||||
*/
|
||||
pushPanZoom: pushPanZoom,
|
||||
|
||||
/**
|
||||
* Pop a pan-zoom state from the stack. Whatever pan-zoom
|
||||
* state was previously present will become current.
|
||||
* If called when there is only one pan-zoom state on the
|
||||
* stack, this acts as a no-op (that is, the lowest
|
||||
* pan-zoom state on the stack cannot be popped, to ensure
|
||||
* that some pan-zoom state is always available.)
|
||||
*/
|
||||
popPanZoom: popPanZoom,
|
||||
|
||||
/**
|
||||
* Set the base pan-zoom state; that is, the state at the
|
||||
* bottom of the stack. This allows the "unzoomed" state of
|
||||
* a plot to be updated (e.g. as new data comes in) without
|
||||
* interfering with the user's chosen zoom level.
|
||||
* @param {number[]} origin the base origin
|
||||
* @param {number[]} dimensions the base dimensions
|
||||
*/
|
||||
setBasePanZoom: setBasePanZoom,
|
||||
|
||||
/**
|
||||
* Clear the pan-zoom stack down to its bottom element;
|
||||
* in effect, pop all elements but the last, e.g. to remove
|
||||
* any temporary user modifications to pan-zoom state.
|
||||
*/
|
||||
clearPanZoom: clearPanZoom,
|
||||
|
||||
/**
|
||||
* Get the current pan-zoom state (the state at the top
|
||||
* of the stack), expressed as an object with "origin" and
|
||||
* "dimensions" fields.
|
||||
* @returns {object} the current pan-zoom state
|
||||
*/
|
||||
getPanZoom: getPanZoom,
|
||||
|
||||
/**
|
||||
* Get the current origin, as represented on the top of the
|
||||
* stack.
|
||||
* @returns {number[]} the current plot origin
|
||||
*/
|
||||
getOrigin: getOrigin,
|
||||
|
||||
/**
|
||||
* Get the current dimensions, as represented on the top of
|
||||
* the stack.
|
||||
* @returns {number[]} the current plot dimensions
|
||||
*/
|
||||
getDimensions: getDimensions
|
||||
};
|
||||
}
|
||||
|
||||
return PlotPanZoomStack;
|
||||
}
|
||||
);
|
76
platform/features/plot/src/elements/PlotPosition.js
Normal file
76
platform/features/plot/src/elements/PlotPosition.js
Normal file
@ -0,0 +1,76 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A PlotPosition converts from pixel coordinates to domain-range
|
||||
* coordinates, based on the current plot boundary as described on
|
||||
* the pan-zoom stack.
|
||||
*
|
||||
* These coordinates are not updated after construction; that is,
|
||||
* they represent the result of the conversion at the time the
|
||||
* PlotPosition was instantiated. Care should be taken when retaining
|
||||
* PlotPosition objects across changes to the pan-zoom stack.
|
||||
*
|
||||
* @constructor
|
||||
* @param {number} x the horizontal pixel position in the plot area
|
||||
* @param {number} y the vertical pixel position in the plot area
|
||||
* @param {number} width the width of the plot area
|
||||
* @param {number} height the height of the plot area
|
||||
* @param {PanZoomStack} panZoomStack the applicable pan-zoom stack,
|
||||
* used to determine the plot's domain-range boundaries.
|
||||
*/
|
||||
function PlotPosition(x, y, width, height, panZoomStack) {
|
||||
var panZoom = panZoomStack.getPanZoom(),
|
||||
origin = panZoom.origin,
|
||||
dimensions = panZoom.dimensions,
|
||||
position;
|
||||
|
||||
function convert(v, i) {
|
||||
return v * dimensions[i] + origin[i];
|
||||
}
|
||||
|
||||
if (!dimensions || !origin) {
|
||||
// We need both dimensions and origin to compute a position
|
||||
position = [];
|
||||
} else {
|
||||
// Convert from pixel to domain-range space.
|
||||
// Note that range is reversed from the y-axis in pixel space
|
||||
//(positive range points up, positive pixel-y points down)
|
||||
position = [ x / width, (height - y) / height ].map(convert);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the domain value corresponding to this pixel position.
|
||||
* @returns {number} the domain value
|
||||
*/
|
||||
getDomain: function () {
|
||||
return position[0];
|
||||
},
|
||||
/**
|
||||
* Get the range value corresponding to this pixel position.
|
||||
* @returns {number} the range value
|
||||
*/
|
||||
getRange: function () {
|
||||
return position[1];
|
||||
},
|
||||
/**
|
||||
* Get the domain and values corresponding to this
|
||||
* pixel position.
|
||||
* @returns {number[]} an array containing the domain and
|
||||
* the range value, in that order
|
||||
*/
|
||||
getPosition: function () {
|
||||
return position;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return PlotPosition;
|
||||
}
|
||||
);
|
124
platform/features/plot/src/elements/PlotPreparer.js
Normal file
124
platform/features/plot/src/elements/PlotPreparer.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*global define,Float32Array*/
|
||||
|
||||
/**
|
||||
* Prepares data to be rendered in a GL Plot. Handles
|
||||
* the conversion from data API to displayable buffers.
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
function identity(x) { return x; }
|
||||
|
||||
/**
|
||||
* The PlotPreparer is responsible for handling data sets and
|
||||
* preparing them to be rendered. It creates a WebGL-plottable
|
||||
* Float32Array for each trace, and tracks the boundaries of the
|
||||
* data sets (since this is convenient to do during the same pass).
|
||||
* @constructor
|
||||
* @param {Telemetry[]} datas telemetry data objects
|
||||
* @param {string} domain the key to use when looking up domain values
|
||||
* @param {string} range the key to use when looking up range values
|
||||
*/
|
||||
function PlotPreparer(datas, domain, range) {
|
||||
var index,
|
||||
vertices = [],
|
||||
max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
x,
|
||||
y,
|
||||
domainOffset = Number.POSITIVE_INFINITY,
|
||||
buffers;
|
||||
|
||||
// Remove any undefined data sets
|
||||
datas = (datas || []).filter(identity);
|
||||
|
||||
// Do a first pass to determine the domain offset.
|
||||
// This will be use to reduce the magnitude of domain values
|
||||
// in the buffer, to minimize loss-of-precision when
|
||||
// converting to a 32-bit float.
|
||||
datas.forEach(function (data) {
|
||||
domainOffset = Math.min(data.getDomainValue(0, domain), domainOffset);
|
||||
});
|
||||
|
||||
// Assemble buffers, and track bounds of the data present
|
||||
datas.forEach(function (data, i) {
|
||||
vertices.push([]);
|
||||
for (index = 0; index < data.getPointCount(); index += 1) {
|
||||
x = data.getDomainValue(index, domain);
|
||||
y = data.getRangeValue(index, range);
|
||||
vertices[i].push(x - domainOffset);
|
||||
vertices[i].push(y);
|
||||
min[0] = Math.min(min[0], x);
|
||||
min[1] = Math.min(min[1], y);
|
||||
max[0] = Math.max(max[0], x);
|
||||
max[1] = Math.max(max[1], y);
|
||||
}
|
||||
});
|
||||
|
||||
// If range is empty, add some padding
|
||||
if (max[1] === min[1]) {
|
||||
max[1] = max[1] + 1.0;
|
||||
min[1] = min[1] - 1.0;
|
||||
}
|
||||
|
||||
// Convert to Float32Array
|
||||
buffers = vertices.map(function (v) { return new Float32Array(v); });
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the dimensions which bound all data in the provided
|
||||
* data sets. This is given as a two-element array where the
|
||||
* first element is domain, and second is range.
|
||||
* @returns {number[]} the dimensions which bound this data set
|
||||
*/
|
||||
getDimensions: function () {
|
||||
return [max[0] - min[0], max[1] - min[1]];
|
||||
},
|
||||
/**
|
||||
* Get the origin of this data set's boundary.
|
||||
* This is given as a two-element array where the
|
||||
* first element is domain, and second is range.
|
||||
* The domain value here is not adjusted by the domain offset.
|
||||
* @returns {number[]} the origin of this data set's boundary
|
||||
*/
|
||||
getOrigin: function () {
|
||||
return min;
|
||||
},
|
||||
/**
|
||||
* Get the domain offset; this offset will have been subtracted
|
||||
* from all domain values in all buffers returned by this
|
||||
* preparer, in order to minimize loss-of-precision due to
|
||||
* conversion to the 32-bit float format needed by WebGL.
|
||||
* @returns {number} the domain offset
|
||||
*/
|
||||
getDomainOffset: function () {
|
||||
return domainOffset;
|
||||
},
|
||||
/**
|
||||
* Get all renderable buffers for this data set. This will
|
||||
* be returned as an array which can be correlated back to
|
||||
* the provided telemetry data objects (from the constructor
|
||||
* call) by index.
|
||||
*
|
||||
* Internally, these are flattened; each buffer contains a
|
||||
* sequence of alternating domain and range values.
|
||||
*
|
||||
* All domain values in all buffers will have been adjusted
|
||||
* from their original values by subtraction of the domain
|
||||
* offset; this minimizes loss-of-precision resulting from
|
||||
* the conversion to 32-bit floats, which may otherwise
|
||||
* cause aliasing artifacts (particularly for timestamps)
|
||||
*
|
||||
* @returns {Float32Array[]} the buffers for these traces
|
||||
*/
|
||||
getBuffers: function () {
|
||||
return buffers;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PlotPreparer;
|
||||
|
||||
}
|
||||
);
|
75
platform/features/plot/src/elements/PlotTickGenerator.js
Normal file
75
platform/features/plot/src/elements/PlotTickGenerator.js
Normal file
@ -0,0 +1,75 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The PlotTickGenerator provides labels for ticks along the
|
||||
* domain and range axes of the plot, to support the plot
|
||||
* template.
|
||||
*
|
||||
* @constructor
|
||||
* @param {PlotPanZoomStack} panZoomStack the pan-zoom stack for
|
||||
* this plot, used to determine plot boundaries
|
||||
* @param {PlotFormatter} formatter used to format (for display)
|
||||
* domain and range values.
|
||||
*/
|
||||
function PlotTickGenerator(panZoomStack, formatter) {
|
||||
|
||||
// Generate ticks; interpolate from start up to
|
||||
// start + span in count steps, using the provided
|
||||
// formatter to represent each value.
|
||||
function generateTicks(start, span, count, format) {
|
||||
var step = span / (count - 1),
|
||||
result = [],
|
||||
i;
|
||||
|
||||
for (i = 0; i < count; i += 1) {
|
||||
result.push({
|
||||
label: format(i * step + start)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
/**
|
||||
* Generate tick marks for the domain axis.
|
||||
* @param {number} count the number of ticks
|
||||
* @returns {string[]} labels for those ticks
|
||||
*/
|
||||
generateDomainTicks: function (count) {
|
||||
var panZoom = panZoomStack.getPanZoom();
|
||||
return generateTicks(
|
||||
panZoom.origin[0],
|
||||
panZoom.dimensions[0],
|
||||
count,
|
||||
formatter.formatDomainValue
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate tick marks for the range axis.
|
||||
* @param {number} count the number of ticks
|
||||
* @returns {string[]} labels for those ticks
|
||||
*/
|
||||
generateRangeTicks: function (count) {
|
||||
var panZoom = panZoomStack.getPanZoom();
|
||||
return generateTicks(
|
||||
panZoom.origin[1],
|
||||
panZoom.dimensions[1],
|
||||
count,
|
||||
formatter.formatRangeValue
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return PlotTickGenerator;
|
||||
}
|
||||
);
|
111
platform/features/plot/test/GLChartSpec.js
Normal file
111
platform/features/plot/test/GLChartSpec.js
Normal file
@ -0,0 +1,111 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../src/GLChart"],
|
||||
function (GLChart) {
|
||||
"use strict";
|
||||
|
||||
describe("A WebGL chart", function () {
|
||||
var mockCanvas,
|
||||
mockGL,
|
||||
glChart;
|
||||
|
||||
beforeEach(function () {
|
||||
mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]);
|
||||
mockGL = jasmine.createSpyObj(
|
||||
"gl",
|
||||
[
|
||||
"createShader",
|
||||
"compileShader",
|
||||
"shaderSource",
|
||||
"attachShader",
|
||||
"createProgram",
|
||||
"linkProgram",
|
||||
"useProgram",
|
||||
"enableVertexAttribArray",
|
||||
"getAttribLocation",
|
||||
"getUniformLocation",
|
||||
"createBuffer",
|
||||
"lineWidth",
|
||||
"enable",
|
||||
"blendFunc",
|
||||
"viewport",
|
||||
"clear",
|
||||
"uniform2fv",
|
||||
"uniform4fv",
|
||||
"bufferData",
|
||||
"bindBuffer",
|
||||
"vertexAttribPointer",
|
||||
"drawArrays"
|
||||
]
|
||||
);
|
||||
mockGL.ARRAY_BUFFER = "ARRAY_BUFFER";
|
||||
mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW";
|
||||
mockGL.TRIANGLE_FAN = "TRIANGLE_FAN";
|
||||
mockGL.LINE_STRIP = "LINE_STRIP";
|
||||
|
||||
// Echo back names for uniform locations, so we can
|
||||
// test which of these are set for certain operations.
|
||||
mockGL.getUniformLocation.andCallFake(function (a, name) {
|
||||
return name;
|
||||
});
|
||||
|
||||
mockCanvas.getContext.andReturn(mockGL);
|
||||
|
||||
glChart = new GLChart(mockCanvas);
|
||||
});
|
||||
|
||||
it("allows the canvas to be cleared", function () {
|
||||
glChart.clear();
|
||||
expect(mockGL.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doees not construct if WebGL is unavailable", function () {
|
||||
mockCanvas.getContext.andReturn(undefined);
|
||||
expect(function () {
|
||||
return new GLChart(mockCanvas);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("allows dimensions to be set", function () {
|
||||
glChart.setDimensions([120, 120], [0, 10]);
|
||||
expect(mockGL.uniform2fv)
|
||||
.toHaveBeenCalledWith("uDimensions", [120, 120]);
|
||||
expect(mockGL.uniform2fv)
|
||||
.toHaveBeenCalledWith("uOrigin", [0, 10]);
|
||||
});
|
||||
|
||||
it("allows lines to be drawn", function () {
|
||||
var testBuffer = [ 0, 1, 3, 8 ],
|
||||
testColor = [ 0.25, 0.33, 0.66, 1.0 ],
|
||||
testPoints = 2;
|
||||
glChart.drawLine(testBuffer, testColor, testPoints);
|
||||
expect(mockGL.bufferData).toHaveBeenCalledWith(
|
||||
mockGL.ARRAY_BUFFER,
|
||||
testBuffer,
|
||||
mockGL.DYNAMIC_DRAW
|
||||
);
|
||||
expect(mockGL.uniform4fv)
|
||||
.toHaveBeenCalledWith("uColor", testColor);
|
||||
expect(mockGL.drawArrays)
|
||||
.toHaveBeenCalledWith("LINE_STRIP", 0, testPoints);
|
||||
});
|
||||
|
||||
it("allows squares to be drawn", function () {
|
||||
var testMin = [0, 1],
|
||||
testMax = [10, 10],
|
||||
testColor = [ 0.25, 0.33, 0.66, 1.0 ];
|
||||
|
||||
glChart.drawSquare(testMin, testMax, testColor);
|
||||
|
||||
expect(mockGL.uniform4fv)
|
||||
.toHaveBeenCalledWith("uColor", testColor);
|
||||
expect(mockGL.drawArrays)
|
||||
.toHaveBeenCalledWith("TRIANGLE_FAN", 0, 4);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
155
platform/features/plot/test/MCTChartSpec.js
Normal file
155
platform/features/plot/test/MCTChartSpec.js
Normal file
@ -0,0 +1,155 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../src/MCTChart"],
|
||||
function (MCTChart) {
|
||||
"use strict";
|
||||
|
||||
describe("The mct-chart directive", function () {
|
||||
var mockInterval,
|
||||
mockLog,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockCanvas,
|
||||
mockGL,
|
||||
mctChart;
|
||||
|
||||
beforeEach(function () {
|
||||
mockInterval =
|
||||
jasmine.createSpy("$interval");
|
||||
mockLog =
|
||||
jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
|
||||
mockScope =
|
||||
jasmine.createSpyObj("$scope", ["$watchCollection"]);
|
||||
mockElement =
|
||||
jasmine.createSpyObj("element", ["find"]);
|
||||
|
||||
|
||||
// mct-chart uses GLChart, so it needs WebGL API
|
||||
mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]);
|
||||
mockGL = jasmine.createSpyObj(
|
||||
"gl",
|
||||
[
|
||||
"createShader",
|
||||
"compileShader",
|
||||
"shaderSource",
|
||||
"attachShader",
|
||||
"createProgram",
|
||||
"linkProgram",
|
||||
"useProgram",
|
||||
"enableVertexAttribArray",
|
||||
"getAttribLocation",
|
||||
"getUniformLocation",
|
||||
"createBuffer",
|
||||
"lineWidth",
|
||||
"enable",
|
||||
"blendFunc",
|
||||
"viewport",
|
||||
"clear",
|
||||
"uniform2fv",
|
||||
"uniform4fv",
|
||||
"bufferData",
|
||||
"bindBuffer",
|
||||
"vertexAttribPointer",
|
||||
"drawArrays"
|
||||
]
|
||||
);
|
||||
mockGL.ARRAY_BUFFER = "ARRAY_BUFFER";
|
||||
mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW";
|
||||
mockGL.TRIANGLE_FAN = "TRIANGLE_FAN";
|
||||
mockGL.LINE_STRIP = "LINE_STRIP";
|
||||
|
||||
// Echo back names for uniform locations, so we can
|
||||
// test which of these are set for certain operations.
|
||||
mockGL.getUniformLocation.andCallFake(function (a, name) {
|
||||
return name;
|
||||
});
|
||||
|
||||
mockElement.find.andReturn([mockCanvas]);
|
||||
mockCanvas.getContext.andReturn(mockGL);
|
||||
|
||||
mctChart = new MCTChart(mockInterval, mockLog);
|
||||
});
|
||||
|
||||
it("is applicable at the element level", function () {
|
||||
expect(mctChart.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("places a 'draw' attribute in-scope", function () {
|
||||
// Should ask Angular for the draw attribute
|
||||
expect(mctChart.scope.draw).toEqual("=");
|
||||
});
|
||||
|
||||
it("watches for changes in the drawn object", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockScope.$watchCollection)
|
||||
.toHaveBeenCalledWith("draw", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("issues one draw call per line", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]({
|
||||
lines: [ {}, {}, {} ]
|
||||
});
|
||||
expect(mockGL.drawArrays.calls.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("issues one draw call per box", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]({
|
||||
boxes: [
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] }
|
||||
]
|
||||
});
|
||||
expect(mockGL.drawArrays.calls.length).toEqual(4);
|
||||
});
|
||||
|
||||
it("does not fail if no draw object is in scope", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockScope.$watchCollection.mostRecentCall.args[1])
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it("draws on canvas resize", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
|
||||
// Should track canvas size in an interval
|
||||
expect(mockInterval).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
jasmine.any(Number)
|
||||
);
|
||||
|
||||
// Verify pre-condition
|
||||
expect(mockGL.clear).not.toHaveBeenCalled();
|
||||
|
||||
mockCanvas.width = 100;
|
||||
mockCanvas.offsetWidth = 150;
|
||||
mockCanvas.height = 200;
|
||||
mockCanvas.offsetHeight = 200;
|
||||
mockInterval.mostRecentCall.args[0]();
|
||||
|
||||
// Use clear as an indication that drawing has occurred
|
||||
expect(mockGL.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns if no WebGL context is available", function () {
|
||||
mockCanvas.getContext.andReturn(undefined);
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockLog.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs nothing in nominal situations (WebGL available)", function () {
|
||||
// Complement the previous test
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
196
platform/features/plot/test/PlotControllerSpec.js
Normal file
196
platform/features/plot/test/PlotControllerSpec.js
Normal file
@ -0,0 +1,196 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../src/PlotController"],
|
||||
function (PlotController) {
|
||||
"use strict";
|
||||
|
||||
describe("The plot controller", function () {
|
||||
var mockScope,
|
||||
mockTelemetry, // mock telemetry controller
|
||||
mockData,
|
||||
mockElement,
|
||||
controller;
|
||||
|
||||
function echo(i) { return i; }
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch", "$on" ]
|
||||
);
|
||||
mockTelemetry = jasmine.createSpyObj(
|
||||
"telemetry",
|
||||
[ "getResponse", "getMetadata" ]
|
||||
);
|
||||
mockData = jasmine.createSpyObj(
|
||||
"data",
|
||||
[ "getPointCount", "getDomainValue", "getRangeValue" ]
|
||||
);
|
||||
mockElement = jasmine.createSpyObj(
|
||||
"element",
|
||||
[ "getBoundingClientRect" ]
|
||||
);
|
||||
|
||||
mockScope.telemetry = mockTelemetry;
|
||||
mockTelemetry.getResponse.andReturn([mockData]);
|
||||
mockData.getPointCount.andReturn(2);
|
||||
mockData.getDomainValue.andCallFake(echo);
|
||||
mockData.getRangeValue.andCallFake(echo);
|
||||
mockElement.getBoundingClientRect.andReturn({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 100,
|
||||
height: 100
|
||||
});
|
||||
|
||||
controller = new PlotController(mockScope);
|
||||
});
|
||||
|
||||
it("listens for telemetry updates", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"telemetryUpdate",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("provides plot colors", function () {
|
||||
// PlotPalette will have its own tests
|
||||
expect(controller.getColor(0))
|
||||
.toEqual(jasmine.any(String));
|
||||
|
||||
// Colors should be unique
|
||||
expect(controller.getColor(0))
|
||||
.not.toEqual(controller.getColor(1));
|
||||
});
|
||||
|
||||
it("draws lines when telemetry data becomes available", function () {
|
||||
// Broadcast data
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Should have put some lines in the drawing scope,
|
||||
// which the template should pass along to the renderer
|
||||
expect(mockScope.draw.lines).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not fail if telemetry controller is not in scope", function () {
|
||||
mockScope.telemetry = undefined;
|
||||
|
||||
// Broadcast data
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Just want to not have an exception
|
||||
});
|
||||
|
||||
it("provides coordinates on hover", function () {
|
||||
expect(controller.getHoverCoordinates().length).toEqual(0);
|
||||
|
||||
controller.hover({
|
||||
target: mockElement
|
||||
});
|
||||
|
||||
expect(controller.getHoverCoordinates().length).toEqual(2);
|
||||
});
|
||||
|
||||
it("permits marquee zoom", function () {
|
||||
// Verify pre-condition
|
||||
expect(controller.isZoomed()).toBeFalsy();
|
||||
|
||||
// Simulate a marquee zoom interaction
|
||||
controller.startMarquee({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 10
|
||||
});
|
||||
|
||||
controller.hover({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 0
|
||||
});
|
||||
|
||||
controller.endMarquee({
|
||||
target: mockElement,
|
||||
clientX: 10,
|
||||
clientY: 0
|
||||
});
|
||||
|
||||
expect(controller.isZoomed()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("permits unøom", function () {
|
||||
// Simulate a marquee zoom interaction
|
||||
controller.startMarquee({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 10
|
||||
});
|
||||
|
||||
controller.hover({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 0
|
||||
});
|
||||
|
||||
controller.endMarquee({
|
||||
target: mockElement,
|
||||
clientX: 10,
|
||||
clientY: 0
|
||||
});
|
||||
|
||||
// Verify precondition
|
||||
expect(controller.isZoomed()).toBeTruthy();
|
||||
|
||||
// Perform the unzoom
|
||||
controller.unzoom();
|
||||
|
||||
// Should no longer report as zoomed
|
||||
expect(controller.isZoomed()).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
it("permits unøom", function () {
|
||||
// Simulate two marquee zooms interaction
|
||||
[0, 1].forEach(function (n) {
|
||||
controller.startMarquee({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 10 + 10 * n
|
||||
});
|
||||
|
||||
controller.hover({
|
||||
target: mockElement,
|
||||
clientX: 0,
|
||||
clientY: 0
|
||||
});
|
||||
|
||||
controller.endMarquee({
|
||||
target: mockElement,
|
||||
clientX: 10 + 10 * n,
|
||||
clientY: 0
|
||||
});
|
||||
});
|
||||
|
||||
// Verify precondition
|
||||
expect(controller.isZoomed()).toBeTruthy();
|
||||
|
||||
// Step back...
|
||||
controller.stepBackPanZoom();
|
||||
|
||||
// Should still be zoomed
|
||||
expect(controller.isZoomed()).toBeTruthy();
|
||||
|
||||
// Step back again...
|
||||
controller.stepBackPanZoom();
|
||||
|
||||
// Should no longer report as zoomed
|
||||
expect(controller.isZoomed()).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
62
platform/features/plot/test/elements/PlotAxisSpec.js
Normal file
62
platform/features/plot/test/elements/PlotAxisSpec.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotAxis"],
|
||||
function (PlotAxis) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot axis", function () {
|
||||
var testMetadatas = [
|
||||
{
|
||||
tests: [
|
||||
{ key: "t0", name: "T0" },
|
||||
{ key: "t1", name: "T1" }
|
||||
],
|
||||
someKey: "some value"
|
||||
},
|
||||
{
|
||||
tests: [
|
||||
{ key: "t0", name: "T0" },
|
||||
{ key: "t2", name: "T2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
tests: [
|
||||
{ key: "t3", name: "T3" },
|
||||
{ key: "t4", name: "T4" },
|
||||
{ key: "t5", name: "T5" },
|
||||
{ key: "t6", name: "T6" }
|
||||
]
|
||||
}
|
||||
],
|
||||
testDefault = { key: "test", name: "Test" },
|
||||
controller = new PlotAxis("tests", testMetadatas, testDefault);
|
||||
|
||||
it("pulls out a list of domain or range options", function () {
|
||||
// Should have filtered out duplicates, etc
|
||||
expect(controller.options).toEqual([
|
||||
{ key: "t0", name: "T0" },
|
||||
{ key: "t1", name: "T1" },
|
||||
{ key: "t2", name: "T2" },
|
||||
{ key: "t3", name: "T3" },
|
||||
{ key: "t4", name: "T4" },
|
||||
{ key: "t5", name: "T5" },
|
||||
{ key: "t6", name: "T6" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("chooses the first option as a default", function () {
|
||||
expect(controller.active).toEqual({ key: "t0", name: "T0" });
|
||||
});
|
||||
|
||||
it("falls back to a provided default if no options are present", function () {
|
||||
expect(new PlotAxis("tests", [{}], testDefault).active)
|
||||
.toEqual(testDefault);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
29
platform/features/plot/test/elements/PlotFormatterSpec.js
Normal file
29
platform/features/plot/test/elements/PlotFormatterSpec.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotFormatter"],
|
||||
function (PlotFormatter) {
|
||||
"use strict";
|
||||
|
||||
describe("The plot formatter", function () {
|
||||
var formatter;
|
||||
|
||||
beforeEach(function () {
|
||||
formatter = new PlotFormatter();
|
||||
});
|
||||
|
||||
it("formats domains using YYYY-DDD style", function () {
|
||||
expect(formatter.formatDomainValue(402513731000)).toEqual(
|
||||
"1982-276 17:22:11"
|
||||
);
|
||||
});
|
||||
|
||||
it("formats ranges as values", function () {
|
||||
expect(formatter.formatRangeValue(10)).toEqual("10.0");
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
104
platform/features/plot/test/elements/PlotPaletteSpec.js
Normal file
104
platform/features/plot/test/elements/PlotPaletteSpec.js
Normal file
@ -0,0 +1,104 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotPalette"],
|
||||
function (PlotPalette) {
|
||||
"use strict";
|
||||
|
||||
describe("The plot palette", function () {
|
||||
it("can be used as a constructor", function () {
|
||||
// PlotPalette has all static methods, so make
|
||||
// sure it returns itself if used as a constructor.
|
||||
expect(new PlotPalette()).toBe(PlotPalette);
|
||||
});
|
||||
|
||||
it("has 30 unique colors in an integer format", function () {
|
||||
// Integer format may be useful internal to the application.
|
||||
// RGB 0-255
|
||||
var i, j;
|
||||
|
||||
// Used to verify one of R, G, B in loop below
|
||||
function verifyChannel(c) {
|
||||
expect(typeof c).toEqual("number");
|
||||
expect(c <= 255).toBeTruthy();
|
||||
expect(c >= 0).toBeTruthy();
|
||||
}
|
||||
|
||||
for (i = 0; i < 30; i += 1) {
|
||||
// Verify that we got an array of numbers
|
||||
expect(Array.isArray(PlotPalette.getIntegerColor(i)))
|
||||
.toBeTruthy();
|
||||
expect(PlotPalette.getIntegerColor(i).length).toEqual(3);
|
||||
|
||||
// Verify all three channels for type and range
|
||||
PlotPalette.getIntegerColor(i).forEach(verifyChannel);
|
||||
|
||||
// Verify uniqueness
|
||||
for (j = i + 1; j < 30; j += 1) {
|
||||
expect(PlotPalette.getIntegerColor(i)).not.toEqual(
|
||||
PlotPalette.getIntegerColor(j)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it("has 30 unique colors in a floating-point format", function () {
|
||||
// Float format is useful to WebGL.
|
||||
// RGB 0.0-1.1
|
||||
var i, j;
|
||||
|
||||
// Used to verify one of R, G, B in loop below
|
||||
function verifyChannel(c) {
|
||||
expect(typeof c).toEqual("number");
|
||||
expect(c <= 1.0).toBeTruthy();
|
||||
expect(c >= 0.0).toBeTruthy();
|
||||
}
|
||||
|
||||
for (i = 0; i < 30; i += 1) {
|
||||
// Verify that we got an array of numbers
|
||||
expect(Array.isArray(PlotPalette.getFloatColor(i)))
|
||||
.toBeTruthy();
|
||||
expect(PlotPalette.getFloatColor(i).length).toEqual(4);
|
||||
|
||||
// Verify all three channels for type and range
|
||||
PlotPalette.getFloatColor(i).forEach(verifyChannel);
|
||||
|
||||
// Verify uniqueness
|
||||
for (j = i + 1; j < 30; j += 1) {
|
||||
expect(PlotPalette.getFloatColor(i)).not.toEqual(
|
||||
PlotPalette.getFloatColor(j)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it("has 30 unique colors in a string format", function () {
|
||||
// String format is useful in stylesheets
|
||||
// #RRGGBB in hex
|
||||
var i, j, c;
|
||||
|
||||
|
||||
for (i = 0; i < 30; i += 1) {
|
||||
c = PlotPalette.getStringColor(i);
|
||||
|
||||
// Verify that we #-style color strings
|
||||
expect(typeof c).toEqual('string');
|
||||
expect(c.length).toEqual(7);
|
||||
expect(/^#[0-9a-fA-F]+$/.test(c)).toBeTruthy();
|
||||
|
||||
// Verify uniqueness
|
||||
for (j = i + 1; j < 30; j += 1) {
|
||||
expect(PlotPalette.getStringColor(i)).not.toEqual(
|
||||
PlotPalette.getStringColor(j)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
80
platform/features/plot/test/elements/PlotPanZoomStackSpec.js
Normal file
80
platform/features/plot/test/elements/PlotPanZoomStackSpec.js
Normal file
@ -0,0 +1,80 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotPanZoomStack"],
|
||||
function (PlotPanZoomStack) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot pan-zoom stack", function () {
|
||||
var panZoomStack,
|
||||
initialOrigin,
|
||||
initialDimensions,
|
||||
otherOrigins,
|
||||
otherDimensions;
|
||||
|
||||
// Shorthand for verifying getOrigin, getDimensions, and getPanZoom,
|
||||
// which should always agree.
|
||||
function verifyPanZoom(origin, dimensions) {
|
||||
expect(panZoomStack.getOrigin()).toEqual(origin);
|
||||
expect(panZoomStack.getDimensions()).toEqual(dimensions);
|
||||
expect(panZoomStack.getPanZoom()).toEqual({
|
||||
origin: origin,
|
||||
dimensions: dimensions
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
initialOrigin = [ 4, 2 ];
|
||||
initialDimensions = [ 600, 400 ];
|
||||
otherOrigins = [ [8, 6], [12, 9] ];
|
||||
otherDimensions = [ [400, 300], [200, 300] ];
|
||||
panZoomStack =
|
||||
new PlotPanZoomStack(initialOrigin, initialDimensions);
|
||||
});
|
||||
|
||||
it("starts off reporting its initial values", function () {
|
||||
verifyPanZoom(initialOrigin, initialDimensions);
|
||||
});
|
||||
|
||||
it("allows origin/dimensions pairs to be pushed/popped", function () {
|
||||
panZoomStack.pushPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
verifyPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
panZoomStack.pushPanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
verifyPanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
panZoomStack.popPanZoom();
|
||||
verifyPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
panZoomStack.popPanZoom();
|
||||
verifyPanZoom(initialOrigin, initialDimensions);
|
||||
});
|
||||
|
||||
it("reports current stack depth", function () {
|
||||
expect(panZoomStack.getDepth()).toEqual(1);
|
||||
panZoomStack.pushPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
expect(panZoomStack.getDepth()).toEqual(2);
|
||||
panZoomStack.pushPanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
expect(panZoomStack.getDepth()).toEqual(3);
|
||||
});
|
||||
|
||||
it("allows base pan zoom to be restored", function () {
|
||||
panZoomStack.pushPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
panZoomStack.pushPanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
panZoomStack.clearPanZoom();
|
||||
verifyPanZoom(initialOrigin, initialDimensions);
|
||||
});
|
||||
|
||||
it("allows base pan zoom to be changed", function () {
|
||||
panZoomStack.pushPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
panZoomStack.setBasePanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
// Should not have changed current top-of-stack
|
||||
verifyPanZoom(otherOrigins[0], otherDimensions[0]);
|
||||
|
||||
// Clear the stack - should be at our new base pan-zoom state
|
||||
panZoomStack.clearPanZoom();
|
||||
verifyPanZoom(otherOrigins[1], otherDimensions[1]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
48
platform/features/plot/test/elements/PlotPositionSpec.js
Normal file
48
platform/features/plot/test/elements/PlotPositionSpec.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotPosition"],
|
||||
function (PlotPosition) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot position", function () {
|
||||
var mockPanZoom,
|
||||
testOrigin = [ 10, 20 ],
|
||||
testDimensions = [ 800, 10 ];
|
||||
|
||||
beforeEach(function () {
|
||||
mockPanZoom = jasmine.createSpyObj(
|
||||
"panZoomStack",
|
||||
[ "getPanZoom" ]
|
||||
);
|
||||
mockPanZoom.getPanZoom.andReturn({
|
||||
origin: testOrigin,
|
||||
dimensions: testDimensions
|
||||
});
|
||||
});
|
||||
|
||||
it("transforms pixel coordinates to domain-range", function () {
|
||||
var position = new PlotPosition(42, 450, 100, 1000, mockPanZoom);
|
||||
// Domain: .42 * 800 + 10 = 346
|
||||
// Range: .55 * 10 + 20 = 25.5
|
||||
// Notably, y-axis is reversed between pixel space and range
|
||||
expect(position.getPosition()).toEqual([346, 25.5]);
|
||||
expect(position.getDomain()).toEqual(346);
|
||||
expect(position.getRange()).toEqual(25.5);
|
||||
});
|
||||
|
||||
it("treats a position as undefined if no pan-zoom state is present", function () {
|
||||
var position;
|
||||
|
||||
mockPanZoom.getPanZoom.andReturn({});
|
||||
position = new PlotPosition(1, 2, 100, 100, mockPanZoom);
|
||||
expect(position.getDomain()).toBeUndefined();
|
||||
expect(position.getRange()).toBeUndefined();
|
||||
expect(position.getPosition()).toEqual([]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
65
platform/features/plot/test/elements/PlotPreparerSpec.js
Normal file
65
platform/features/plot/test/elements/PlotPreparerSpec.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotPreparer"],
|
||||
function (PlotPreparer) {
|
||||
"use strict";
|
||||
|
||||
var START = 123456;
|
||||
|
||||
describe("A plot preparer", function () {
|
||||
|
||||
function makeMockData(scale) {
|
||||
var mockData = jasmine.createSpyObj(
|
||||
"data" + scale,
|
||||
[ "getPointCount", "getDomainValue", "getRangeValue" ]
|
||||
);
|
||||
mockData.getPointCount.andReturn(1000);
|
||||
mockData.getDomainValue.andCallFake(function (i) {
|
||||
return START + i * 1000;
|
||||
});
|
||||
mockData.getRangeValue.andCallFake(function (i) {
|
||||
return Math.sin(i / 100) * scale;
|
||||
});
|
||||
return mockData;
|
||||
}
|
||||
|
||||
it("fits to provided data sets", function () {
|
||||
var datas = [1, 2, 3].map(makeMockData),
|
||||
preparer = new PlotPreparer(datas);
|
||||
|
||||
expect(preparer.getDomainOffset()).toEqual(START);
|
||||
expect(preparer.getOrigin()[0]).toBeCloseTo(START, 3);
|
||||
expect(preparer.getOrigin()[1]).toBeCloseTo(-3, 3);
|
||||
expect(preparer.getDimensions()[0]).toBeCloseTo(999000, 3);
|
||||
expect(preparer.getDimensions()[1]).toBeCloseTo(6, 3);
|
||||
});
|
||||
|
||||
it("looks up values using a specified domain and range", function () {
|
||||
var datas = [makeMockData(1)],
|
||||
preparer = new PlotPreparer(datas, "testDomain", "testRange");
|
||||
|
||||
expect(datas[0].getDomainValue).toHaveBeenCalledWith(
|
||||
jasmine.any(Number),
|
||||
"testDomain"
|
||||
);
|
||||
|
||||
expect(datas[0].getRangeValue).toHaveBeenCalledWith(
|
||||
jasmine.any(Number),
|
||||
"testRange"
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a default range if data set is flat", function () {
|
||||
var datas = [makeMockData(0)],
|
||||
preparer = new PlotPreparer(datas);
|
||||
|
||||
expect(preparer.getDimensions[1]).not.toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,54 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotTickGenerator"],
|
||||
function (PlotTickGenerator) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot tick generator", function () {
|
||||
var mockPanZoomStack,
|
||||
mockFormatter,
|
||||
generator;
|
||||
|
||||
beforeEach(function () {
|
||||
mockPanZoomStack = jasmine.createSpyObj(
|
||||
"panZoomStack",
|
||||
[ "getPanZoom" ]
|
||||
);
|
||||
mockFormatter = jasmine.createSpyObj(
|
||||
"formatter",
|
||||
[ "formatDomainValue", "formatRangeValue" ]
|
||||
);
|
||||
|
||||
mockPanZoomStack.getPanZoom.andReturn({
|
||||
origin: [ 0, 0 ],
|
||||
dimensions: [ 100, 100 ]
|
||||
});
|
||||
|
||||
generator =
|
||||
new PlotTickGenerator(mockPanZoomStack, mockFormatter);
|
||||
});
|
||||
|
||||
it("provides tick marks for range", function () {
|
||||
expect(generator.generateRangeTicks(11).length).toEqual(11);
|
||||
|
||||
// Should have used range formatter
|
||||
expect(mockFormatter.formatRangeValue).toHaveBeenCalled();
|
||||
expect(mockFormatter.formatDomainValue).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
it("provides tick marks for domain", function () {
|
||||
expect(generator.generateDomainTicks(11).length).toEqual(11);
|
||||
|
||||
// Should have used domain formatter
|
||||
expect(mockFormatter.formatRangeValue).not.toHaveBeenCalled();
|
||||
expect(mockFormatter.formatDomainValue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
13
platform/features/plot/test/suite.json
Normal file
13
platform/features/plot/test/suite.json
Normal file
@ -0,0 +1,13 @@
|
||||
[
|
||||
"GLChart",
|
||||
"MCTChart",
|
||||
"PlotController",
|
||||
"elements/PlotAxis",
|
||||
"elements/PlotFormatter",
|
||||
"elements/PlotPalette",
|
||||
"elements/PlotPanZoomStack",
|
||||
"elements/PlotPosition",
|
||||
"elements/PlotPreparer",
|
||||
"elements/PlotTickGenerator"
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user