[Plot] Begin refactoring for stacked plots

Begin refactoring Plot view to support stacked plots;
separate out behavior which needs to occur per-plot
from area which should occur per-view, and add a
handler for the Overlay mode. WTD-625.
This commit is contained in:
Victor Woeltjen 2014-12-10 19:51:27 -08:00
parent 1c3d2d923f
commit 784b2b6186
5 changed files with 307 additions and 202 deletions

View File

@ -6,7 +6,7 @@
<span ng-repeat="subplot in plot.getSubPlots()">
<div class="gl-plot-legend">
<span class='plot-legend-item'
ng-repeat="telemetryObject in subplot.telemetryObjects">
ng-repeat="telemetryObject in subplot.getTelemetryObjects()">
<span class='plot-color-swatch'
ng-style="{ 'background-color': plot.getColor($index) }">
</span>
@ -16,7 +16,7 @@
<div class="gl-plot-coords"
ng-if="representation.showControls">
{{plot.getHoverCoordinates().join(', ')}}
{{subplot.getHoverCoordinates().join(', ')}}
</div>
<div class="gl-plot-axis-area gl-plot-y">
@ -48,17 +48,17 @@
ng-mouseenter="representation.showControls = true">
<div class="gl-plot-hash hash-v"
ng-repeat="tick in subplot.domainTicks"
ng-style="{ left: (100 * $index / (subplot.domainTicks.length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (subplot.domainTicks.length - 1)">
ng-repeat="tick in subplot.getDomainTicks()"
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)">
</div>
<div class="gl-plot-hash hash-h"
ng-repeat="tick in rangeTicks"
ng-style="{ bottom: (100 * $index / (subplot.rangeTicks.length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (subplot.rangeTicks.length - 1)">
ng-repeat="tick in subplot.getRangeTicks()"
ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (subplot.getRangeTicks().length - 1)">
</div>
<mct-chart draw="subplot.draw"
<mct-chart draw="subplot.getDrawingObject()"
ng-mousemove="subplot.hover($event)"
ng-mousedown="subplot.startMarquee($event)"
ng-mouseup="subplot.endMarquee($event)">
@ -117,10 +117,10 @@
</div>
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x">
<div ng-repeat="tick in domainTicks"
<div ng-repeat="tick in subplot.getDomainTicks()"
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)) + '%' }">
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)"
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%' }">
{{tick.label}}
</div>

View File

@ -45,94 +45,10 @@ define(
* @constructor
*/
function PlotController($scope) {
var mousePosition,
marqueeStart,
panZoomStack = new PlotPanZoomStack([], []),
formatter = new PlotFormatter(),
modeOptions,
var modeOptions = new PlotModeOptions([]),
subplots = [],
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) {
@ -171,63 +87,16 @@ define(
($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();
modeOptions.getModeHandler().plotTelemetry(prepared);
}
function setupModes(telemetryObjects) {
modeOptions = new PlotModeOptions(telemetryObjects);
modeOptions = new PlotModeOptions(telemetryObjects || []);
}
$scope.$watch("telemetry.getTelemetryObjects()", setupModes);
$scope.$watch("telemetry.getMetadata()", setupAxes);
$scope.$on("telemetryUpdate", plotTelemetry);
$scope.draw = {};
return {
/**
@ -239,49 +108,6 @@ define(
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
@ -289,36 +115,34 @@ define(
* @returns {boolean} true if not in default state
*/
isZoomed: function () {
return panZoomStack.getDepth() > 1;
return modeOptions.getModeHandler().isZoomed();
},
/**
* Undo the most recent pan/zoom change and restore
* the prior state.
*/
stepBackPanZoom: function () {
panZoomStack.popPanZoom();
updateDrawingBounds();
return modeOptions.getModeHandler().stepBackPanZoom();
},
/**
* Undo all pan/zoom changes and restore the initial state.
*/
unzoom: function () {
panZoomStack.clearPanZoom();
updateDrawingBounds();
return modeOptions.getModeHandler().unzoom();
},
/**
* Get the mode options (Stacked/Overlaid) that are applicable
* for this plot.
*/
getModeOptions: function () {
return modeOptions && modeOptions.getModeOptions();
return modeOptions.getModeOptions();
},
/**
* Get the current mode that is applicable to this plot. This
* will include key, name, and glyph fields.
*/
getMode: function () {
return modeOptions && modeOptions.getMode();
return modeOptions.getMode();
},
/**
* Set the mode which should be active in this plot.
@ -326,7 +150,15 @@ define(
* getModeOptions()
*/
setMode: function (mode) {
return modeOptions && modeOptions.setMode(mode);
return modeOptions.setMode(mode);
},
/**
* Get all individual plots contained within this Plot view.
* (Multiple may be contained when in Stacked mode).
* @returns {SubPlot[]} all subplots in this Plot view
*/
getSubPlots: function () {
return modeOptions.getModeHandler().getSubPlots();
}
};

View File

@ -0,0 +1,201 @@
/*global define*/
define(
['elements/PlotPosition', 'elements/PlotFormatter', 'elements/PlotTickGenerator'],
function (PlotPosition, PlotFormatter, PlotTickGenerator) {
"use strict";
var AXIS_DEFAULTS = [
{ "name": "Time" },
{ "name": "Value" }
],
DOMAIN_TICKS = 5,
RANGE_TICKS = 7;
function SubPlot(telemetryObjects, panZoomStack) {
var draw = {},
rangeTicks = [],
domainTicks = [],
formatter = new PlotFormatter(),
domainOffset,
mousePosition,
marqueeStart;
// 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.
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.
draw.dimensions = panZoom.dimensions;
draw.origin = [
panZoom.origin[0] - domainOffset,
panZoom.origin[1]
];
}
// Update tick marks in scope.
function updateTicks() {
var tickGenerator = new PlotTickGenerator(panZoomStack, formatter);
domainTicks =
tickGenerator.generateDomainTicks(DOMAIN_TICKS);
rangeTicks =
tickGenerator.generateRangeTicks(RANGE_TICKS);
}
// 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();
}
return {
getTelemetryObjects: function () {
return telemetryObjects;
},
getDomainTicks: function () {
return domainTicks;
},
getRangeTicks: function () {
return rangeTicks;
},
getDrawingObject: function () {
return 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
*/
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();
}
},
/**
* Update the drawing bounds, marquee box, and
* tick marks for this subplot.
*/
update: function () {
updateDrawingBounds();
updateMarqueeBox();
updateTicks();
}
};
}
return SubPlot;
}
);

View File

@ -8,21 +8,30 @@ define(
var STACKED = {
key: "stacked",
name: "Stacked",
glyph: "8"
glyph: "8",
factory: PlotOverlayMode
},
OVERLAID = {
key: "overlaid",
name: "Overlaid",
glyph: "6"
glyph: "6",
factory: PlotStackedMode
};
function PlotModeOptions(telemetryObjects) {
var options = telemetryObjects.length > 1 ?
[ OVERLAID, STACKED ] : [ OVERLAID ],
mode = options[0];
mode = options[0],
modeHandler;
return {
getModeHandler: function () {
if (!modeHandler) {
modeHandler = mode.factory(telemetryObjects);
}
return modeHandler;
},
getModeOptions: function () {
return options;
},
@ -30,7 +39,10 @@ define(
return mode;
},
setMode: function (option) {
mode = option;
if (mode !== option) {
mode = option;
modeHandler = undefined;
}
}
};
}

View File

@ -0,0 +1,60 @@
/*global define*/
define(
["../SubPlot", "../elements/PlotPalette", "../elements/PlotPanZoomStack"],
function (SubPlot, PlotPalette, PlotPanZoomStack) {
"use strict";
function PlotOverlayMode(telemetryObjects) {
var domainOffset,
panZoomStack = new PlotPanZoomStack([], []),
subplot = new SubPlot(telemetryObjects, panZoomStack),
subplots = [ subplot ];
function plotTelemetry(prepared) {
// 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.
subplot.getDrawingObject().lines = prepared.getBuffers().map(function (buf, i) {
return {
buffer: buf,
color: PlotPalette.getFloatColor(i),
points: buf.length / 2
};
});
subplot.update();
}
return {
plotTelemetry: plotTelemetry,
getSubPlots: function () {
return subplots;
},
isZoomed: function () {
return panZoomStack.getDepth() > 1;
},
stepBackPanZoom: function () {
panZoomStack.pop();
subplot.update();
},
unzoom: function () {
panZoomStack.clearPanZoom();
subplot.update();
}
};
}
return PlotOverlayMode;
}
);