[Plot] Bring in scripts from sandbox branch

Bring in scripts for plotting from the sandbox
branch to begin transitioning/integrating plot
view. WTD-533.
This commit is contained in:
Victor Woeltjen 2014-12-01 09:41:39 -08:00
parent 55c2d15cdc
commit b556b5e4f2
8 changed files with 758 additions and 0 deletions

View 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" ]
}
],
"controllers": [
{
"key": "PlotController",
"implementation": "PlotController.js",
"depends": [ "$scope" ]
}
]
}
}

File diff suppressed because one or more lines are too long

View 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 axes[1].ticks"
class="gl-plot-tick gl-plot-y-tick-label"
ng-style="{ bottom: (100 * $index / (axes[1].ticks.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 axes[0].ticks"
ng-style="{ left: (100 * $index / (axes[0].ticks.length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (axes[0].ticks.length - 1)">
</div>
<div class="gl-plot-hash hash-h"
ng-repeat="tick in axes[1].ticks"
ng-style="{ bottom: (100 * $index / (axes[1].ticks.length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (axes[1].ticks.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">B</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">R</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 axes[0].ticks"
class="gl-plot-tick gl-plot-x-tick-label"
ng-show="$index > 0 && $index < (axes[0].ticks.length - 1)"
ng-style="{ left: (100 * $index / (axes[0].ticks.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>

View File

@ -0,0 +1,99 @@
/*global define,Promise,Float32Array*/
/**
* Module defining GLPlot. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
"use strict";
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');
function GLPlot(canvas) {
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"),
vertexShader,
fragmentShader,
program,
aVertexPosition,
uColor,
uDimensions,
uOrigin,
buffer;
if (!gl) {
return false;
}
// 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);
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
uColor = gl.getUniformLocation(program, "uColor");
uDimensions = gl.getUniformLocation(program, "uDimensions");
uOrigin = gl.getUniformLocation(program, "uOrigin");
buffer = gl.createBuffer();
gl.lineWidth(2.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
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: function () {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT);
},
setDimensions: function (dimensions, origin) {
gl.uniform2fv(uDimensions, dimensions);
gl.uniform2fv(uOrigin, origin);
},
drawLine: function (buf, color, points) {
doDraw(gl.LINE_STRIP, buf, color, points);
},
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);
},
gl: gl
};
}
return GLPlot;
}
);

View File

@ -0,0 +1,92 @@
/*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; }
function GLPlotPreparer(datas, domain, range) {
var index,
vertices = [],
max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
x,
y,
xLabels = {},
yLabels = {},
yUnits = {},
domainOffset = Number.POSITIVE_INFINITY,
buffers;
datas = datas || [];
datas.filter(identity).forEach(function (data) {
domainOffset = Math.min(data.getDomainValue(0, domain), domainOffset);
});
datas.forEach(function (data, i) {
if (!data) {
return; // skip null data
}
vertices.push([]);
for (index = 0; index < data.getPointCount(); index = index + 1) {
x = data.getDomainValue(index, domain) - domainOffset;
y = data.getRangeValue(index, range);
vertices[i].push(x);
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 (data.getDomainLabel) {
xLabels[data.getDomainLabel(domain)] = true;
}
if (data.getRangeLabel) {
yLabels[data.getRangeLabel(range)] = true;
}
if (data.getRangeUnits) {
yUnits[data.getRangeUnits(range)] = true;
}
});
if (max[1] === min[1]) {
max[1] = max[1] + 1.0;
min[1] = min[1] - 1.0;
}
xLabels = Object.keys(xLabels).sort();
yLabels = Object.keys(yLabels).sort();
yUnits = Object.keys(yUnits).sort();
buffers = vertices.map(function (v) { return new Float32Array(v); });
return {
getDimensions: function () {
return [max[0] - min[0], max[1] - min[1]];
},
getOrigin: function () {
return min;
},
getDomainOffset: function () {
return domainOffset;
},
getBuffers: function () {
return buffers;
}
};
}
return GLPlotPreparer;
}
);

View File

@ -0,0 +1,78 @@
/*global define,Promise*/
/**
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
*/
define(
["./GLPlot"],
function (GLPlot) {
"use strict";
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
/**
*
* @constructor
*/
function MCTChart($interval) {
function linkChart(scope, element) {
var canvas = element.find("canvas")[0],
chart = new GLPlot(canvas);
function doDraw() {
var draw = scope.draw;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
chart.clear();
if (!draw) {
return;
}
chart.setDimensions(
draw.dimensions || [1, 1],
draw.origin || [0, 0]
);
(draw.lines || []).forEach(function (line) {
chart.drawLine(
line.buffer,
line.color,
line.points
);
});
(draw.boxes || []).forEach(function (box) {
chart.drawSquare(
box.start,
box.end,
box.color
);
});
}
function drawIfResized() {
if (canvas.width !== canvas.offsetWidth ||
canvas.height !== canvas.offsetHeight) {
doDraw();
}
}
$interval(drawIfResized, 1000);
scope.$watchCollection("draw", doDraw);
}
return {
restrict: "E",
template: TEMPLATE,
link: linkChart,
scope: { draw: "=" }
};
}
return MCTChart;
}
);

View File

@ -0,0 +1,270 @@
/*global define,moment,Promise*/
/**
* Module defining PlotController. Created by vwoeltje on 11/12/14.
*/
define(
["./GLPlotPreparer", "./PlotPalette", "../lib/moment.min.js"],
function (GLPlotPreparer, PlotPalette) {
"use strict";
var AXIS_DEFAULTS = [
{ "name": "Time" },
{ "name": "Value" }
],
DOMAIN_TICKS = 5,
RANGE_TICKS = 7;
/**
*
* @constructor
*/
function PlotController($scope) {
var mousePosition,
marqueeStart,
panZoomStack = [{
dimensions: [],
origin: []
}],
domainOffset;
function formatDomainValue(v) {
return moment.utc(v).format("YYYY-DDD HH:mm:ss");
}
function formatRangeValue(v) {
return v.toFixed(1);
}
// Utility, for map/forEach loops. Index 0 is domain,
// index 1 is range.
function formatValue(v, i) {
return (i ? formatRangeValue : formatDomainValue)(v);
}
function pixelToDomainRange(x, y, width, height, domainOffset) {
var panZoom = panZoomStack[panZoomStack.length - 1],
offset = [ domainOffset || 0, 0],
origin = panZoom.origin,
dimensions = panZoom.dimensions;
if (!dimensions || !origin) {
return [];
}
return [ x / width, (height - y) / height ].map(function (v, i) {
return v * dimensions[i] + origin[i] + offset[i];
});
}
function mousePositionToDomainRange(mousePosition, domainOffset) {
return pixelToDomainRange(
mousePosition.x,
mousePosition.y,
mousePosition.width,
mousePosition.height,
domainOffset
);
}
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;
}
function updateMarqueeBox() {
$scope.draw.boxes = marqueeStart ?
[{
start: mousePositionToDomainRange(marqueeStart),
end: mousePositionToDomainRange(mousePosition),
color: [1, 1, 1, 0.5 ]
}] : undefined;
}
function updateDrawingBounds() {
var panZoom = panZoomStack[panZoomStack.length - 1];
$scope.draw.dimensions = panZoom.dimensions;
$scope.draw.origin = panZoom.origin;
}
function plotTelemetry() {
var telemetry, prepared, data;
telemetry = $scope.telemetry;
if (!telemetry) {
return;
}
data = telemetry.getResponse();
prepared = new GLPlotPreparer(
data,
($scope.axes[0].active || {}).key,
($scope.axes[1].active || {}).key
);
$scope.axes[0].ticks = generateTicks(
prepared.getOrigin()[0] + prepared.getDomainOffset(),
prepared.getDimensions()[0],
DOMAIN_TICKS,
formatDomainValue
);
$scope.axes[1].ticks = generateTicks(
prepared.getOrigin()[1],
prepared.getDimensions()[1],
RANGE_TICKS,
formatRangeValue
);
panZoomStack[0] = {
origin: prepared.getOrigin(),
dimensions: prepared.getDimensions()
};
domainOffset = prepared.getDomainOffset();
$scope.draw.lines = prepared.getBuffers().map(function (buf, i) {
return {
buffer: buf,
color: PlotPalette.getFloatColor(i),
points: buf.length / 2
};
});
updateDrawingBounds();
updateMarqueeBox();
}
function setupAxes(metadatas) {
var domainKeys = {},
rangeKeys = {},
domains = [],
ranges = [];
function buildOptionsForMetadata(m) {
(m.domains || []).forEach(function (domain) {
if (!domainKeys[domain.key]) {
domainKeys[domain.key] = true;
domains.push(domain);
}
});
(m.ranges || []).forEach(function (range) {
if (!rangeKeys[range.key]) {
rangeKeys[range.key] = true;
ranges.push(range);
}
});
}
(metadatas || []).
forEach(buildOptionsForMetadata);
[domains, ranges].forEach(function (options, i) {
var active = $scope.axes[i].active;
$scope.axes[i].options = options;
if (!active || !active.key) {
$scope.axes[i].active =
options[0] || AXIS_DEFAULTS[i];
}
});
}
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
};
}
function marqueeZoom(start, end) {
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]
];
panZoomStack.push({
origin: origin,
dimensions: dimensions
});
}
$scope.axes = [ {}, {} ];
$scope.$watch("telemetry.getMetadata()", setupAxes);
$scope.$on("telemetryUpdate", plotTelemetry);
$scope.draw = {};
return {
getColor: function (index) {
return PlotPalette.getStringColor(index);
},
getHoverCoordinates: function () {
return mousePosition ?
mousePositionToDomainRange(
mousePosition,
domainOffset
).map(formatValue) : [];
},
hover: function ($event) {
mousePosition = toMousePosition($event);
if (marqueeStart) {
updateMarqueeBox();
}
},
startMarquee: function ($event) {
mousePosition = marqueeStart = toMousePosition($event);
updateMarqueeBox();
},
endMarquee: function ($event) {
mousePosition = toMousePosition($event);
if (marqueeStart) {
marqueeZoom(marqueeStart, mousePosition);
marqueeStart = undefined;
updateMarqueeBox();
updateDrawingBounds();
}
},
isZoomed: function () {
return panZoomStack.length > 1;
},
stepBackPanZoom: function () {
if (panZoomStack.length > 1) {
panZoomStack.pop();
updateDrawingBounds();
}
},
unzoom: function () {
panZoomStack = [panZoomStack[0]];
updateDrawingBounds();
}
};
}
return PlotController;
}
);

View File

@ -0,0 +1,65 @@
/*global define*/
/**
* Plot palette. Defines colors for various plot lines.
*/
define(
function () {
'use strict';
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
});
return {
getIntegerColor: function (i) {
return integerPalette[Math.floor(i) % integerPalette.length];
},
getFloatColor: function (i) {
return floatPalette[Math.floor(i) % floatPalette.length];
},
getStringColor: function (i) {
return stringPalette[Math.floor(i) % stringPalette.length];
}
};
}
);