Files
openmct/platform/features/plot/src/SubPlot.js
Henry 3cf62ded08 Plot axes do not show labels when no data
Added semicolon

Added test for hasDomainData()

Added test for negative case on domain data check

Removed label test due to mocked telemetry formatter

Uncommented test commented by accident

Removed extra space
2015-10-01 16:35:14 -07:00

418 lines
16 KiB
JavaScript

/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[
'./elements/PlotPosition',
'./elements/PlotTickGenerator'
],
function (PlotPosition, PlotTickGenerator) {
"use strict";
var DOMAIN_TICKS = 5,
RANGE_TICKS = 7;
/**
* A SubPlot is an individual plot within a Plot View (which
* may contain multiple plots, specifically when in Stacked
* plot mode.)
* @memberof platform/features/plot
* @constructor
* @param {DomainObject[]} telemetryObjects the domain objects
* which will be plotted in this sub-plot
* @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom
* states which is applicable to this sub-plot
* @param {TelemetryFormatter} telemetryFormatter the telemetry
* formatting service; used to convert domain/range values
* from telemetry data sets to a human-readable form.
*/
function SubPlot(telemetryObjects, panZoomStack, telemetryFormatter) {
// We are used from a template often, so maintain
// state in local variables to allow for fast look-up,
// as is normal for controllers.
this.telemetryObjects = telemetryObjects;
this.domainTicks = [];
this.rangeTicks = [];
this.formatter = telemetryFormatter;
this.draw = {};
this.hovering = false;
this.panZoomStack = panZoomStack;
// Start with the right initial drawing bounds,
// tick marks
this.updateDrawingBounds();
this.updateTicks();
}
/**
* Tests whether this subplot has domain data to show for the current pan/zoom level. Absence of domain data
* implies that there is no range data displayed either
* @returns {boolean} true if domain data exists for the current pan/zoom level
*/
SubPlot.prototype.hasDomainData = function() {
return this.panZoomStack
&& this.panZoomStack.getDimensions()[0] > 0;
};
// Utility function for filtering out empty strings.
function isNonEmpty(v) {
return typeof v === 'string' && v !== "";
}
// Converts from pixel coordinates to domain-range,
// to interpret mouse gestures.
SubPlot.prototype.mousePositionToDomainRange = function (mousePosition) {
return new PlotPosition(
mousePosition.x,
mousePosition.y,
mousePosition.width,
mousePosition.height,
this.panZoomStack
).getPosition();
};
// Utility function to get the mouse position (in x,y
// pixel coordinates in the canvas area) from a mouse
// event object.
SubPlot.prototype.toMousePosition = function ($event) {
var bounds = this.subPlotBounds;
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.)
SubPlot.prototype.toDisplayable = function (position) {
return [ position[0] - this.domainOffset, position[1] ];
};
// Update the current hover coordinates
SubPlot.prototype.updateHoverCoordinates = function () {
var formatter = this.formatter;
// Utility, for map/forEach loops. Index 0 is domain,
// index 1 is range.
function formatValue(v, i) {
return (i ?
formatter.formatRangeValue :
formatter.formatDomainValue)(v);
}
this.hoverCoordinates = this.mousePosition &&
this.mousePositionToDomainRange(this.mousePosition)
.map(formatValue)
.filter(isNonEmpty)
.join(", ");
};
// Update the drawable marquee area to reflect current
// mouse position (or don't show it at all, if no marquee
// zoom is in progress)
SubPlot.prototype.updateMarqueeBox = function () {
// Express this as a box in the draw object, which
// is passed to an mct-chart in the template for rendering.
this.draw.boxes = this.marqueeStart ?
[{
start: this.toDisplayable(
this.mousePositionToDomainRange(this.marqueeStart)
),
end: this.toDisplayable(
this.mousePositionToDomainRange(this.mousePosition)
),
color: [1, 1, 1, 0.5 ]
}] : undefined;
};
// Update the bounds (origin and dimensions) of the drawing area.
SubPlot.prototype.updateDrawingBounds = function () {
var panZoom = this.panZoomStack.getPanZoom();
// Communicate pan-zoom state from stack to the draw object
// which is passed to mct-chart in the template.
this.draw.dimensions = panZoom.dimensions;
this.draw.origin = [
panZoom.origin[0] - this.domainOffset,
panZoom.origin[1]
];
};
// Update tick marks in scope.
SubPlot.prototype.updateTicks = function () {
var tickGenerator =
new PlotTickGenerator(this.panZoomStack, this.formatter);
this.domainTicks =
tickGenerator.generateDomainTicks(DOMAIN_TICKS);
this.rangeTicks =
tickGenerator.generateRangeTicks(RANGE_TICKS);
};
SubPlot.prototype.updatePan = function () {
var start, current, delta, nextOrigin;
// Clear the previous panning pan-zoom state
this.panZoomStack.popPanZoom();
// Calculate what the new resulting pan-zoom should be
start = this.mousePositionToDomainRange(
this.panStart,
this.panZoomStack
);
current = this.mousePositionToDomainRange(
this.mousePosition,
this.panZoomStack
);
delta = [ current[0] - start[0], current[1] - start[1] ];
nextOrigin = [
this.panStartBounds.origin[0] - delta[0],
this.panStartBounds.origin[1] - delta[1]
];
// ...and push a new one at the current mouse position
this.panZoomStack
.pushPanZoom(nextOrigin, this.panStartBounds.dimensions);
};
/**
* Get the set of domain objects which are being
* represented in this sub-plot.
* @returns {DomainObject[]} the domain objects which
* will have data plotted in this sub-plot
*/
SubPlot.prototype.getTelemetryObjects = function () {
return this.telemetryObjects;
};
/**
* Get ticks mark information appropriate for using in the
* template for this sub-plot's domain axis, as prepared
* by the PlotTickGenerator.
* @returns {Array} tick marks for the domain axis
*/
SubPlot.prototype.getDomainTicks = function () {
return this.domainTicks;
};
/**
* Get ticks mark information appropriate for using in the
* template for this sub-plot's range axis, as prepared
* by the PlotTickGenerator.
* @returns {Array} tick marks for the range axis
*/
SubPlot.prototype.getRangeTicks = function () {
return this.rangeTicks;
};
/**
* Get the drawing object associated with this sub-plot;
* this object will be passed to the mct-chart in which
* this sub-plot's lines will be plotted, as its "draw"
* attribute, and should have the same internal format
* expected by that directive.
* @return {object} the drawing object
*/
SubPlot.prototype.getDrawingObject = function () {
return this.draw;
};
/**
* 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
*/
SubPlot.prototype.getHoverCoordinates = function () {
return this.hoverCoordinates;
};
/**
* Handle mouse movement over the chart area.
* @param $event the mouse event
* @memberof platform/features/plot.SubPlot#
*/
SubPlot.prototype.hover = function ($event) {
this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
//If there is a domain to display, show hover coordinates, otherwise hover coordinates are meaningless
if (this.hasDomainData()) {
this.updateHoverCoordinates();
}
if (this.marqueeStart) {
this.updateMarqueeBox();
}
if (this.panStart) {
this.updatePan();
this.updateDrawingBounds();
this.updateTicks();
}
};
/**
* Continue a previously-start pan or zoom gesture.
* @param $event the mouse event
* @memberof platform/features/plot.SubPlot#
*/
SubPlot.prototype.continueDrag = function ($event) {
this.mousePosition = this.toMousePosition($event);
if (this.marqueeStart) {
this.updateMarqueeBox();
}
if (this.panStart) {
this.updatePan();
this.updateDrawingBounds();
this.updateTicks();
}
};
/**
* Initiate a marquee zoom action.
* @param $event the mouse event
*/
SubPlot.prototype.startDrag = function ($event) {
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
// Treat any modifier key as a pan
if ($event.altKey || $event.shiftKey || $event.ctrlKey) {
// Start panning
this.panStart = this.mousePosition;
this.panStartBounds = this.panZoomStack.getPanZoom();
// We're starting a pan, so add this back as a
// state on the stack; it will get replaced
// during the pan.
this.panZoomStack.pushPanZoom(
this.panStartBounds.origin,
this.panStartBounds.dimensions
);
$event.preventDefault();
} else {
// Start marquee zooming
this.marqueeStart = this.mousePosition;
this.updateMarqueeBox();
}
};
/**
* Complete a marquee zoom action.
* @param $event the mouse event
*/
SubPlot.prototype.endDrag = function ($event) {
var self = this;
// 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 = self.mousePositionToDomainRange(start),
b = self.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]
];
// Proceed with zoom if zoom dimensions are non zeros
if (!(dimensions[0] === 0 && dimensions[1] === 0)) {
// Push the new state onto the pan-zoom stack
self.panZoomStack.pushPanZoom(origin, dimensions);
// Make sure tick marks reflect new bounds
self.updateTicks();
}
}
this.mousePosition = this.toMousePosition($event);
this.subPlotBounds = undefined;
if (this.marqueeStart) {
marqueeZoom(this.marqueeStart, this.mousePosition);
this.marqueeStart = undefined;
this.updateMarqueeBox();
this.updateDrawingBounds();
this.updateTicks();
}
if (this.panStart) {
// End panning
this.panStart = undefined;
this.panStartBounds = undefined;
}
};
/**
* Update the drawing bounds, marquee box, and
* tick marks for this subplot.
*/
SubPlot.prototype.update = function () {
this.updateDrawingBounds();
this.updateMarqueeBox();
this.updateTicks();
};
/**
* Set the domain offset associated with this sub-plot.
* A domain offset is subtracted from all domain
* before lines are drawn to avoid artifacts associated
* with the use of 32-bit floats when domain values
* are often timestamps (due to insufficient precision.)
* A SubPlot will be drawing boxes (for marquee zoom) in
* the same offset coordinate space, so it needs to know
* the value of this to position that marquee box
* correctly.
* @param {number} value the domain offset
*/
SubPlot.prototype.setDomainOffset = function (value) {
this.domainOffset = value;
};
/**
* When used with no argument, check whether or not the user
* is currently hovering over this subplot. When used with
* an argument, set that state.
* @param {boolean} [state] the new hovering state
* @returns {boolean} the hovering state
*/
SubPlot.prototype.isHovering = function (state) {
if (state !== undefined) {
this.hovering = state;
}
return this.hovering;
};
return SubPlot;
}
);