Plots refactor to use Vue and es6 instead of angular (#3586)

* Initial commit of plot refactor for vuejs

* Use es6 classes instead of using extend

* Use classList api to add and remove classes

* Remove angular specific event mechanisms

* Refactor plot legend into smaller components

* Refactor moving config into MctPlot component. Fix Legend issues.

* Refactor XAxis and YAxis into their own components

* Remove commented out code

* Remove empty initialize method

* Fix grid lines and initialize function revert.

* Check that plots views are available only to domainObjects that have range and domain

* Make css class a computed property

* Remove unnecessary legacyObject conversion

* Remove comments and commented out code

* Remove use of private for vue methods

* Remove console logs

* Fixes Y-axis ticks display

* Adds stacked plots and overlay plots

* Fix css for stacked plots

* Disable Vue plots

* Rename Stacked plot item component

* Address Review comment: Remove unnecessary event emitted

* Address review comments: Add a note about why nextTick is needed

* Fix bug with legend when multiple plots are being displayed

* Change LinearScale to a class

* Remove duplicated comment

* Adds missing copyright info

* Revert change to stackedplotItem

* Adds basic tests

* Fixes broken test.

* Adds new test

* Fix linting errors. Adds tests

* Adds tests

* Adds tests for stacked plots

* Adds more tests

* Removes fdescribe

* Adds tests for y-axis ticks

* Tests for addition of series to plots

* Adds more tests

* Adds cursor guides test

* Adds tests for interceptors

* Adds more plots tests for x and y scale

* Use config store

* Adding goToOriginalAction tests

* Modified tests to increase coverage, and added teardown for application router

* Fixed linting issues

Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Shefali Joshi 2021-02-19 06:48:17 -08:00 committed by GitHub
parent 6226763c37
commit 1499286bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 6689 additions and 40 deletions

View File

@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("the plugin", () => {
let openmct;
let goToFolderAction;
let mockObjectPath;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
goToFolderAction = openmct.actions._allActions.goToOriginal;
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the go to folder action', () => {
expect(goToFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(() => {
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
});
});
});

View File

@ -0,0 +1,99 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from "utils/testing";
import InterceptorPlugin from "./plugin";
describe('the plugin', function () {
let element;
let child;
let openmct;
const TEST_NAMESPACE = 'test';
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct));
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('the missingObjectInterceptor', () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
]);
mockProvider.get.and.returnValue(Promise.resolve(undefined));
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
});
it('returns missing objects', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'hello'
};
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
type: 'unknown',
name: 'Missing: test:hello'
});
done();
});
});
it('returns the My items object if not found', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'mine'
};
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
"name": "My Items",
"type": "folder",
"composition": [],
"location": "ROOT"
});
done();
});
});
});
});

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Plot from '../single/Plot.vue';
import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) {
return {
key: 'plot-overlay',
name: 'Overlay Plot',
cssClass: 'icon-telemetry',
canView(domainObject) {
return domainObject.type === 'telemetry.plot.overlay';
},
canEdit(domainObject) {
return domainObject.type === 'telemetry.plot.overlay';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Plot
},
provide: {
openmct,
domainObject
},
template: '<plot></plot>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -0,0 +1,83 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/*jscs:disable disallowDanglingUnderscores */
/**
* A scale has an input domain and an output range. It provides functions
* `scale` return the range value associated with a domain value.
* `invert` return the domain value associated with range value.
*/
class LinearScale {
constructor(domain) {
this.domain(domain);
}
domain(newDomain) {
if (newDomain) {
this._domain = newDomain;
this._domainDenominator = newDomain.max - newDomain.min;
}
return this._domain;
}
range(newRange) {
if (newRange) {
this._range = newRange;
this._rangeDenominator = newRange.max - newRange.min;
}
return this._range;
}
scale(domainValue) {
if (!this._domain || !this._range) {
return;
}
const domainOffset = domainValue - this._domain.min;
const rangeFraction = domainOffset - this._domainDenominator;
const rangeOffset = rangeFraction * this._rangeDenominator;
const rangeValue = rangeOffset + this._range.min;
return rangeValue;
}
invert(rangeValue) {
if (!this._domain || !this._range) {
return;
}
const rangeOffset = rangeValue - this._range.min;
const domainFraction = rangeOffset / this._rangeDenominator;
const domainOffset = domainFraction * this._domainDenominator;
const domainValue = domainOffset + this._domain.min;
return domainValue;
}
}
export default LinearScale;
/**
*
*/

View File

@ -0,0 +1,892 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div v-if="loaded"
class="gl-plot"
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend :cursor-locked="!!lockHighlightPoint"
:series="config.series.models"
:highlights="highlights"
:legend="config.legend"
/>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<y-axis v-if="config.series.models.length > 0"
:tick-width="tickWidth"
:single-series="config.series.models.length === 1"
:series-model="config.series.models[0]"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
/>
<div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{
left: (tickWidth + 20) + 'px'
}"
>
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
<div class="l-state-indicators">
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
></span>
</div>
<mct-ticks v-show="gridLines"
:axis-type="'xAxis'"
:position="'right'"
@plotTickWidth="onTickWidthChange"
/>
<mct-ticks v-show="gridLines"
:axis-type="'yAxis'"
:position="'bottom'"
@plotTickWidth="onTickWidthChange"
/>
<div ref="chartContainer"
class="gl-plot-chart-wrapper"
>
<mct-chart :series-config="config"
:rectangles="rectangles"
:highlights="highlights"
@plotReinitializeCanvas="initCanvas"
/>
</div>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<button class="c-button icon-minus"
title="Zoom out"
@click="zoom('out', 0.2)"
>
</button>
<button class="c-button icon-plus"
title="Zoom in"
@click="zoom('in', 0.2)"
>
</button>
</div>
<div class="c-button-set c-button-set--strip-h"
:disabled="!plotHistory.length"
>
<button class="c-button icon-arrow-left"
title="Restore previous pan/zoom"
@click="back()"
>
</button>
<button class="c-button icon-reset"
title="Reset pan/zoom"
@click="clear()"
>
</button>
</div>
</div>
<!--Cursor guides-->
<div v-show="cursorGuide"
ref="cursorGuideVertical"
class="c-cursor-guide--v js-cursor-guide--v"
>
</div>
<div v-show="cursorGuide"
ref="cursorGuideHorizontal"
class="c-cursor-guide--h js-cursor-guide--h"
>
</div>
</div>
<x-axis v-if="config.series.models.length > 0"
:series-model="config.series.models[0]"
/>
</div>
</div>
</div>
</template>
<script>
import eventHelpers from './lib/eventHelpers';
import LinearScale from "./LinearScale";
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
import configStore from './configuration/configStore';
import PlotLegend from "./legend/PlotLegend.vue";
import MctTicks from "./MctTicks.vue";
import MctChart from "./chart/MctChart.vue";
import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
export default {
components: {
XAxis,
YAxis,
PlotLegend,
MctTicks,
MctChart
},
inject: ['openmct', 'domainObject'],
props: {
gridLines: {
type: Boolean,
default() {
return true;
}
},
cursorGuide: {
type: Boolean,
default() {
return true;
}
},
plotTickWidth: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
highlights: [],
lockHighlightPoint: false,
tickWidth: 0,
yKeyOptions: [],
yAxisLabel: '',
rectangles: [],
plotHistory: [],
selectedXKeyOption: {},
xKeyOptions: [],
config: {},
pending: 0,
loaded: false
};
},
computed: {
plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`;
},
plotLegendExpandedStateClass() {
if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
}
}
},
watch: {
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
},
gridLines(newGridLines) {
this.setGridLinesVisibility(newGridLines);
},
cursorGuide(newCursorGuide) {
this.setCursorGuideVisibility(newCursorGuide);
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
this.config.series.models.forEach(this.addSeries, this);
this.filterObserver = this.openmct.objects.observe(
this.domainObject,
'configuration.filters',
this.updateFiltersAndResubscribe
);
this.openmct.objectViews.on('clearData', this.clearData);
this.followTimeConductor();
this.loaded = true;
//We're referencing the canvas elements from the mct-chart in the initialize method.
// So we need $nextTick to ensure the component is fully mounted before we can initialize stuff.
this.$nextTick(this.initialize);
},
beforeDestroy() {
this.destroy();
},
methods: {
followTimeConductor() {
this.openmct.time.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (!config) {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct
});
configStore.add(configId, config);
}
return config;
},
addSeries(series) {
this.listenTo(series, 'change:xKey', (xKey) => {
this.setDisplayRange(series, xKey);
}, this);
this.listenTo(series, 'change:yKey', () => {
this.loadSeriesData(series);
}, this);
this.listenTo(series, 'change:interpolate', () => {
this.loadSeriesData(series);
}, this);
this.loadSeriesData(series);
},
removeSeries(plotSeries) {
this.stopListening(plotSeries);
},
loadSeriesData(series) {
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series);
return;
}
this.startLoading();
const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key')
};
series.load(options)
.then(this.stopLoading.bind(this));
},
loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => {
this.startLoading();
plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth,
start: range.min,
end: range.max,
domain: this.config.xAxis.get('key')
})
.then(this.stopLoading());
if (purge) {
plotSeries.purgeRecordsOutsideRange(range);
}
});
},
scheduleLoad(series) {
if (!this.scheduledLoads) {
this.startLoading();
this.scheduledLoads = [];
this.checkForSize = setInterval(function () {
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
return;
}
this.stopLoading();
this.scheduledLoads.forEach(this.loadSeriesData, this);
delete this.scheduledLoads;
clearInterval(this.checkForSize);
delete this.checkForSize;
}.bind(this));
}
if (this.scheduledLoads.indexOf(series) === -1) {
this.scheduledLoads.push(series);
}
},
startLoading() {
this.pending += 1;
this.updateLoading();
},
stopLoading() {
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
this.$nextTick().then(() => {
this.pending -= 1;
this.updateLoading();
});
},
updateLoading() {
this.$emit('loadingUpdated', this.pending > 0);
},
updateFiltersAndResubscribe(updatedFilters) {
this.config.series.forEach(function (series) {
series.updateFiltersAndRefresh(updatedFilters[series.keyString]);
});
},
clearData() {
this.config.series.forEach(function (series) {
series.reset();
});
},
setDisplayRange(series, xKey) {
if (this.config.series.length !== 1) {
return;
}
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
/**
* Track latest display bounds. Forces update when not receiving ticks.
*/
updateDisplayBounds(bounds, isTick) {
const newRange = {
min: bounds.start,
max: bounds.end
};
this.config.xAxis.set('range', newRange);
if (!isTick) {
this.skipReloadOnInteraction = true;
this.clear();
this.skipReloadOnInteraction = false;
this.loadMoreData(newRange, true);
} else {
// Drop any data that is more than 1x (max-min) before min.
// Limit these purges to once a second.
if (!this.nextPurge || this.nextPurge < Date.now()) {
const keepRange = {
min: newRange.min - (newRange.max - newRange.min),
max: newRange.max
};
this.config.series.forEach(function (series) {
series.purgeRecordsOutsideRange(keepRange);
});
this.nextPurge = Date.now() + 1000;
}
}
},
/**
* Handle end of user viewport change: load more data for current display
* bounds, and mark view as synchronized if bounds match configured bounds.
*/
userViewportChangeEnd() {
const xDisplayRange = this.config.xAxis.get('displayRange');
const xRange = this.config.xAxis.get('range');
if (!this.skipReloadOnInteraction) {
this.loadMoreData(xDisplayRange);
}
this.synchronized(xRange.min === xDisplayRange.min
&& xRange.max === xDisplayRange.max);
},
/**
* Getter/setter for "synchronized" value. If not synchronized and
* time conductor is in clock mode, will mark objects as unsynced so that
* displays can update accordingly.
*/
synchronized(value) {
if (typeof value !== 'undefined') {
this._synchronized = value;
const isUnsynced = !value && this.openmct.time.clock();
const domainObject = this.openmct.legacyObject(this.domainObject);
if (domainObject.getCapability('status')) {
domainObject.getCapability('status')
.set('timeconductor-unsynced', isUnsynced);
}
}
return this._synchronized;
},
initCanvas() {
if (this.canvas) {
this.stopListening(this.canvas);
}
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
},
initialize() {
// Setup canvas etc.
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
this.pan = undefined;
this.marquee = undefined;
this.chartElementBounds = undefined;
this.tickUpdate = false;
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
this.config.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$refs.cursorGuideVertical;
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
},
onXAxisChange(displayBounds) {
if (displayBounds) {
this.xScale.domain(displayBounds);
}
},
onYAxisChange(displayBounds) {
if (displayBounds) {
this.yScale.domain(displayBounds);
}
},
onTickWidthChange(width, fromDifferentObject) {
if (fromDifferentObject) {
// Always accept tick width if it comes from a different object.
this.tickWidth = width;
} else {
// Otherwise, only accept tick with if it's larger.
const newWidth = Math.max(width, this.tickWidth);
if (newWidth !== this.tickWidth) {
this.tickWidth = newWidth;
}
}
this.$emit('plotTickWidth', this.tickWidth);
},
trackMousePosition(event) {
this.trackChartElementBounds(event);
this.xScale.range({
min: 0,
max: this.chartElementBounds.width
});
this.yScale.range({
min: 0,
max: this.chartElementBounds.height
});
this.positionOverElement = {
x: event.clientX - this.chartElementBounds.left,
y: this.chartElementBounds.height
- (event.clientY - this.chartElementBounds.top)
};
this.positionOverPlot = {
x: this.xScale.invert(this.positionOverElement.x),
y: this.yScale.invert(this.positionOverElement.y)
};
if (this.cursorGuide) {
this.updateCrosshairs(event);
}
this.highlightValues(this.positionOverPlot.x);
this.updateMarquee();
this.updatePan();
event.preventDefault();
},
updateCrosshairs(event) {
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
},
trackChartElementBounds(event) {
if (event.target === this.canvas) {
this.chartElementBounds = event.target.getBoundingClientRect();
}
},
onPlotHighlightSet($e, point) {
if (point === this.highlightPoint) {
return;
}
this.highlightValues(point);
},
highlightValues(point) {
this.highlightPoint = point;
// TODO: used in StackedPlotController
this.$emit('plotHighlightUpdate', point);
if (this.lockHighlightPoint) {
return;
}
if (!point) {
this.highlights = [];
this.config.series.models.forEach(series => delete series.closest);
} else {
this.highlights = this.config.series.models
.filter(series => series.data.length > 0)
.map(series => {
series.closest = series.nearestPoint(point);
return {
series: series,
point: series.closest
};
});
}
},
untrackMousePosition() {
this.positionOverElement = undefined;
this.positionOverPlot = undefined;
this.highlightValues();
},
onMouseDown(event) {
// do not monitor drag events on browser context click
if (event.ctrlKey) {
return;
}
this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
if (event.altKey) {
return this.startPan(event);
} else {
return this.startMarquee(event);
}
},
onMouseUp(event) {
this.stopListening(window, 'mouseup', this.onMouseUp, this);
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
if (this.isMouseClick()) {
this.lockHighlightPoint = !this.lockHighlightPoint;
}
if (this.pan) {
return this.endPan(event);
}
if (this.marquee) {
return this.endMarquee(event);
}
},
isMouseClick() {
if (!this.marquee) {
return false;
}
const { start, end } = this.marquee;
return start.x === end.x && start.y === end.y;
},
updateMarquee() {
if (!this.marquee) {
return;
}
this.marquee.end = this.positionOverPlot;
this.marquee.endPixels = this.positionOverElement;
},
startMarquee(event) {
this.canvas.classList.remove('plot-drag');
this.canvas.classList.add('plot-marquee');
this.trackMousePosition(event);
if (this.positionOverPlot) {
this.freeze();
this.marquee = {
startPixels: this.positionOverElement,
endPixels: this.positionOverElement,
start: this.positionOverPlot,
end: this.positionOverPlot,
color: [1, 1, 1, 0.5]
};
this.rectangles.push(this.marquee);
this.trackHistory();
}
},
endMarquee() {
const startPixels = this.marquee.startPixels;
const endPixels = this.marquee.endPixels;
const marqueeDistance = Math.sqrt(
Math.pow(startPixels.x - endPixels.x, 2)
+ Math.pow(startPixels.y - endPixels.y, 2)
);
// Don't zoom if mouse moved less than 7.5 pixels.
if (marqueeDistance > 7.5) {
this.config.xAxis.set('displayRange', {
min: Math.min(this.marquee.start.x, this.marquee.end.x),
max: Math.max(this.marquee.start.x, this.marquee.end.x)
});
this.config.yAxis.set('displayRange', {
min: Math.min(this.marquee.start.y, this.marquee.end.y),
max: Math.max(this.marquee.start.y, this.marquee.end.y)
});
this.userViewportChangeEnd();
} else {
// A history entry is created by startMarquee, need to remove
// if marquee zoom doesn't occur.
this.plotHistory.pop();
}
this.rectangles = [];
this.marquee = undefined;
},
zoom(zoomDirection, zoomFactor) {
const currentXaxis = this.config.xAxis.get('displayRange');
const currentYaxis = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!currentXaxis || !currentYaxis) {
return;
}
this.freeze();
this.trackHistory();
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
if (zoomDirection === 'in') {
this.config.xAxis.set('displayRange', {
min: currentXaxis.min + xAxisDist,
max: currentXaxis.max - xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
});
} else if (zoomDirection === 'out') {
this.config.xAxis.set('displayRange', {
min: currentXaxis.min - xAxisDist,
max: currentXaxis.max + xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
});
}
this.userViewportChangeEnd();
},
wheelZoom(event) {
const ZOOM_AMT = 0.1;
event.preventDefault();
if (!this.positionOverPlot) {
return;
}
let xDisplayRange = this.config.xAxis.get('displayRange');
let yDisplayRange = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!xDisplayRange || !yDisplayRange) {
return;
}
this.freeze();
window.clearTimeout(this.stillZooming);
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
let xAxisMinDist = xDistMouseToMin / xAxisDist;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
let plotHistoryStep;
if (!plotHistoryStep) {
plotHistoryStep = {
x: xDisplayRange,
y: yDisplayRange
};
}
if (event.wheelDelta < 0) {
this.config.xAxis.set('displayRange', {
min: xDisplayRange.min + ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
} else if (event.wheelDelta >= 0) {
this.config.xAxis.set('displayRange', {
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
}
this.stillZooming = window.setTimeout(function () {
this.plotHistory.push(plotHistoryStep);
plotHistoryStep = undefined;
this.userViewportChangeEnd();
}.bind(this), 250);
},
startPan(event) {
this.canvas.classList.add('plot-drag');
this.canvas.classList.remove('plot-marquee');
this.trackMousePosition(event);
this.freeze();
this.pan = {
start: this.positionOverPlot
};
event.preventDefault();
this.trackHistory();
return false;
},
updatePan() {
// calculate offset between points. Apply that offset to viewport.
if (!this.pan) {
return;
}
const dX = this.pan.start.x - this.positionOverPlot.x;
const dY = this.pan.start.y - this.positionOverPlot.y;
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
this.config.xAxis.set('displayRange', {
min: xRange.min + dX,
max: xRange.max + dX
});
this.config.yAxis.set('displayRange', {
min: yRange.min + dY,
max: yRange.max + dY
});
},
trackHistory() {
this.plotHistory.push({
x: this.config.xAxis.get('displayRange'),
y: this.config.yAxis.get('displayRange')
});
},
endPan() {
this.pan = undefined;
this.userViewportChangeEnd();
},
freeze() {
this.config.yAxis.set('frozen', true);
this.config.xAxis.set('frozen', true);
},
clear() {
this.config.yAxis.set('frozen', false);
this.config.xAxis.set('frozen', false);
this.plotHistory = [];
this.userViewportChangeEnd();
},
back() {
const previousAxisRanges = this.plotHistory.pop();
if (this.plotHistory.length === 0) {
this.clear();
return;
}
this.config.xAxis.set('displayRange', previousAxisRanges.x);
this.config.yAxis.set('displayRange', previousAxisRanges.y);
this.userViewportChangeEnd();
},
setCursorGuideVisibility(cursorGuide) {
this.cursorGuide = cursorGuide === true;
},
setGridLinesVisibility(gridLines) {
this.gridLines = gridLines === true;
},
setYAxisKey(yKey) {
this.config.series.models[0].emit('change:yKey', yKey);
},
destroy() {
configStore.deleteStore(this.config.id);
this.stopListening();
if (this.checkForSize) {
clearInterval(this.checkForSize);
delete this.checkForSize;
}
if (this.filterObserver) {
this.filterObserver();
}
}
}
};
</script>

View File

@ -0,0 +1,269 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div ref="tickContainer"
class="u-contents js-ticks"
>
<div v-if="position === 'left'"
class="gl-plot-tick-wrapper"
>
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-tick gl-plot-x-tick-label"
:style="{
left: (100 * (tick.value - min) / interval) + '%'
}"
:title="tick.fullText || tick.text"
>
{{ tick.text }}
</div>
</div>
<div v-if="position === 'top'"
class="gl-plot-tick-wrapper"
>
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-tick gl-plot-y-tick-label"
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
:title="tick.fullText || tick.text"
style="margin-top: -0.50em; direction: ltr;"
>
<span>{{ tick.text }}</span>
</div>
</div>
<!-- grid lines follow -->
<template v-if="position === 'right'">
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-hash hash-v"
:style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}"
>
</div>
</template>
<template v-if="position === 'bottom'">
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-hash hash-h"
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
>
</div>
</template>
</div>
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import { ticks, commonPrefix, commonSuffix } from "./tickUtils";
import configStore from "./configuration/configStore";
export default {
inject: ['openmct', 'domainObject'],
props: {
axisType: {
type: String,
default() {
return '';
},
required: true
},
position: {
required: true,
type: String,
default() {
return '';
}
}
},
data() {
return {
ticks: []
};
},
mounted() {
eventHelpers.extend(this);
this.axis = this.getAxisFromConfig();
this.tickCount = 4;
this.tickUpdate = false;
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
this.listenTo(this.axis, 'change:key', this.updateTicksForceRegeneration, this);
this.updateTicks();
},
beforeDestroy() {
this.stopListening();
},
methods: {
getAxisFromConfig() {
if (!this.axisType) {
return;
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config[this.axisType];
}
},
/**
* Determine whether ticks should be regenerated for a given range.
* Ticks are updated
* a) if they don't exist,
* b) if existing ticks are outside of given range,
* c) if range exceeds size of tick range by more than one tick step,
* d) if forced to regenerate (ex. changing x-axis metadata).
*
*/
shouldRegenerateTicks(range, forceRegeneration) {
if (forceRegeneration) {
return true;
}
if (!this.tickRange || !this.ticks || !this.ticks.length) {
return true;
}
if (this.tickRange.max > range.max || this.tickRange.min < range.min) {
return true;
}
if (Math.abs(range.max - this.tickRange.max) > this.tickRange.step) {
return true;
}
if (Math.abs(this.tickRange.min - range.min) > this.tickRange.step) {
return true;
}
return false;
},
getTicks() {
const number = this.tickCount;
const clampRange = this.axis.get('values');
const range = this.axis.get('displayRange');
if (clampRange) {
return clampRange.filter(function (value) {
return value <= range.max && value >= range.min;
}, this);
}
return ticks(range.min, range.max, number);
},
updateTicksForceRegeneration() {
this.updateTicks(true);
},
updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange');
if (!range) {
delete this.min;
delete this.max;
delete this.interval;
delete this.tickRange;
this.ticks = [];
delete this.shouldCheckWidth;
return;
}
const format = this.axis.get('format');
if (!format) {
return;
}
this.min = range.min;
this.max = range.max;
this.interval = Math.abs(range.min - range.max);
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
let newTicks = this.getTicks();
this.tickRange = {
min: Math.min.apply(Math, newTicks),
max: Math.max.apply(Math, newTicks),
step: newTicks[1] - newTicks[0]
};
newTicks = newTicks
.map(function (tickValue) {
return {
value: tickValue,
text: format(tickValue)
};
}, this);
if (newTicks.length && typeof newTicks[0].text === 'string') {
const tickText = newTicks.map(function (t) {
return t.text;
});
const prefix = tickText.reduce(commonPrefix);
const suffix = tickText.reduce(commonSuffix);
newTicks.forEach(function (t) {
t.fullText = t.text;
if (suffix.length) {
t.text = t.text.slice(prefix.length, -suffix.length);
} else {
t.text = t.text.slice(prefix.length);
}
});
}
this.ticks = newTicks;
this.shouldCheckWidth = true;
}
this.scheduleTickUpdate();
},
scheduleTickUpdate() {
if (this.tickUpdate) {
return;
}
this.tickUpdate = true;
setTimeout(this.doTickUpdate.bind(this), 0);
},
doTickUpdate() {
if (this.shouldCheckWidth) {
const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');
const tickWidth = Number([].reduce.call(tickElements, function (memo, first) {
return Math.max(memo, first.offsetWidth);
}, 0));
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', tickWidth);
this.shouldCheckWidth = false;
}
this.tickUpdate = false;
}
}
};
</script>

View File

@ -0,0 +1,125 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
>
<div class="c-control-bar">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportPNG()"
>
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
title="Export This View's Data as JPG"
@click="exportJPG()"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
<div ref="plotContainer"
class="l-view-section u-style-receiver js-style-receiver"
>
<div v-show="!!loading"
class="c-loading--overlay loading"
></div>
<mct-plot :grid-lines="gridLines"
:cursor-guide="cursorGuide"
@loadingUpdated="loadingUpdated"
/>
</div>
</div>
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import MctPlot from './MctPlot.vue';
export default {
components: {
MctPlot
},
inject: ['openmct', 'domainObject'],
data() {
return {
//Don't think we need this as it appears to be stacked plot specific
// hideExportButtons: false
cursorGuide: false,
gridLines: true,
loading: false
};
},
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
beforeDestroy() {
this.destroy();
},
methods: {
loadingUpdated(loading) {
this.loading = loading;
},
destroy() {
this.stopListening();
},
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
},
toggleGridLines() {
this.gridLines = !this.gridLines;
}
}
};
</script>

View File

@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Plot from './Plot.vue';
import Vue from 'vue';
export default function PlotViewProvider(openmct) {
function hasTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
return {
key: 'plot-single',
name: 'Plot',
cssClass: 'icon-telemetry',
canView(domainObject) {
return domainObject.type === 'plot-single' || hasTelemetry(domainObject);
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Plot
},
provide: {
openmct,
domainObject
},
template: '<plot></plot>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -0,0 +1,151 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div v-if="loaded"
class="gl-plot-axis-area gl-plot-x has-local-controls"
>
<mct-ticks :axis-type="'xAxis'"
:position="'left'"
@plotTickWidth="onTickWidthChange"
/>
<div
class="gl-plot-label gl-plot-x-label"
:class="{'icon-gear': isEnabledXKeyToggle()}"
>
{{ xAxisLabel }}
</div>
<select
v-show="isEnabledXKeyToggle()"
v-model="selectedXKeyOptionKey"
class="gl-plot-x-label__select local-controls--hidden"
@change="toggleXKeyOption()"
>
<option v-for="option in xKeyOptions"
:key="option.key"
:value="option.key"
>{{ option.name }}
</option>
</select>
</div>
</template>
<script>
import MctTicks from "../MctTicks.vue";
import eventHelpers from '../lib/eventHelpers';
import configStore from "../configuration/configStore";
export default {
components: {
MctTicks
},
inject: ['openmct', 'domainObject'],
props: {
seriesModel: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
selectedXKeyOptionKey: '',
xKeyOptions: [],
xAxis: {},
loaded: false,
xAxisLabel: ''
};
},
mounted() {
eventHelpers.extend(this);
this.xAxis = this.getXAxisFromConfig();
this.loaded = true;
this.setUpXAxisOptions();
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
},
methods: {
isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.clock();
return isSinglePlot && !isFrozen && !inRealTimeMode;
},
getXAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config.xAxis;
}
},
toggleXKeyOption() {
const selectedXKey = this.selectedXKeyOptionKey;
const dataForSelectedXKey = this.seriesModel.data
? this.seriesModel.data[0][selectedXKey]
: undefined;
if (dataForSelectedXKey !== undefined) {
this.xAxis.set('key', selectedXKey);
} else {
this.openmct.notifications.error('Cannot change x-axis view as no data exists for this view type.');
const xAxisKey = this.xAxis.get('key');
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
}
},
getXKeyOption(key) {
return this.xKeyOptions.find(option => option.key === key);
},
syncXAxisToTimeSystem(timeSystem) {
const xAxisKey = this.xAxis.get('key');
if (xAxisKey !== timeSystem.key) {
this.xAxis.set('key', timeSystem.key);
this.xAxis.resetSeries();
this.setUpXAxisOptions();
}
},
setUpXAxisOptions() {
const xAxisKey = this.xAxis.get('key');
this.xKeyOptions = this.seriesModel.metadata
.valuesForHints(['domain'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
this.xAxisLabel = this.xAxis.get('label');
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
}
}
};
</script>

View File

@ -0,0 +1,137 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div v-if="loaded"
class="gl-plot-axis-area gl-plot-y has-local-controls"
:style="{
width: (tickWidth + 20) + 'px'
}"
>
<div v-if="singleSeries"
class="gl-plot-label gl-plot-y-label"
:class="{'icon-gear': (yKeyOptions.length > 1)}"
>{{ yAxisLabel }}
</div>
<select v-if="yKeyOptions.length > 1 && singleSeries"
v-model="yAxisLabel"
class="gl-plot-y-label__select local-controls--hidden"
@change="toggleYAxisLabel"
>
<option v-for="(option, index) in yKeyOptions"
:key="index"
:value="option.name"
:selected="option.name === yAxisLabel"
>
{{ option.name }}
</option>
</select>
<mct-ticks :axis-type="'yAxis'"
class="gl-plot-ticks"
:position="'top'"
@plotTickWidth="onTickWidthChange"
/>
</div>
</template>
<script>
import MctTicks from "../MctTicks.vue";
import configStore from "../configuration/configStore";
export default {
components: {
MctTicks
},
inject: ['openmct', 'domainObject'],
props: {
singleSeries: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
return {};
}
},
tickWidth: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
yAxisLabel: 'none',
loaded: false
};
},
mounted() {
this.yAxis = this.getYAxisFromConfig();
this.loaded = true;
this.setUpYAxisOptions();
},
methods: {
getYAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config.yAxis;
}
},
setUpYAxisOptions() {
this.yKeyOptions = this.seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
// set yAxisLabel if none is set yet
if (this.yAxisLabel === 'none') {
let yKey = this.seriesModel.model.yKey;
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
this.yAxisLabel = yKeyModel.name;
}
},
toggleYAxisLabel() {
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
if (yAxisObject) {
this.$emit('yKeyChanged', yAxisObject.key);
this.yAxis.set('label', this.yAxisLabel);
}
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
}
}
};
</script>

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartAlarmPointSet {
constructor(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.points = [];
eventHelpers.extend(this);
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
append(datum) {
if (datum.mctLimitState) {
this.points.push({
x: this.offset.xVal(datum, this.series),
y: this.offset.yVal(datum, this.series),
datum: datum
});
}
}
remove(datum) {
this.points = this.points.filter(function (p) {
return p.datum !== datum;
});
}
reset() {
this.points = [];
}
destroy() {
this.stopListening();
}
}

View File

@ -0,0 +1,31 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineLinear extends MCTChartSeriesElement {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
}

View File

@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
removePoint(point, index, count) {
if (index > 0 && index / 2 < this.count) {
this.buffer[index + 1] = this.buffer[index - 1];
}
}
vertexCountForPointAtIndex(index) {
if (index === 0 && this.count === 0) {
return 2;
}
return 4;
}
startIndexForPointAtIndex(index) {
if (index === 0) {
return 0;
}
return 2 + ((index - 1) * 4);
}
addPoint(point, start, count) {
if (start === 0 && this.count === 0) {
// First point is easy.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y; // one point
} else if (start === 0 && this.count > 0) {
// Unshifting requires adding an extra point.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
this.buffer[start + 2] = this.buffer[start + 4];
this.buffer[start + 3] = point.y;
} else {
// Appending anywhere in line, insert standard two points.
this.buffer[start] = point.x;
this.buffer[start + 1] = this.buffer[start - 1];
this.buffer[start + 2] = point.x;
this.buffer[start + 3] = point.y;
if (start < this.count * 2) {
// Insert into the middle, need to update the following
// point.
this.buffer[start + 5] = point.y;
}
}
}
}

View File

@ -0,0 +1,32 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
export default class MCTChartPointSet extends MCTChartSeriesElement {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
}

View File

@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartSeriesElement {
constructor(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.buffer = new Float32Array(20000);
this.count = 0;
eventHelpers.extend(this);
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
getBuffer() {
if (this.isTempBuffer) {
this.buffer = new Float32Array(this.buffer);
this.isTempBuffer = false;
}
return this.buffer;
}
color() {
return this.series.get('color');
}
vertexCountForPointAtIndex(index) {
return 2;
}
startIndexForPointAtIndex(index) {
return 2 * index;
}
removeSegments(index, count) {
const target = index;
const start = index + count;
const end = this.count * 2;
this.buffer.copyWithin(target, start, end);
for (let zero = end - count; zero < end; zero++) {
this.buffer[zero] = 0;
}
}
removePoint(point, index, count) {
// by default, do nothing.
}
remove(point, index, series) {
const vertexCount = this.vertexCountForPointAtIndex(index);
const removalPoint = this.startIndexForPointAtIndex(index);
this.removeSegments(removalPoint, vertexCount);
this.removePoint(
this.makePoint(point, series),
removalPoint,
vertexCount
);
this.count -= (vertexCount / 2);
}
makePoint(point, series) {
if (!this.offset.xVal) {
this.chart.setOffset(point, undefined, series);
}
return {
x: this.offset.xVal(point, series),
y: this.offset.yVal(point, series)
};
}
append(point, index, series) {
const pointsRequired = this.vertexCountForPointAtIndex(index);
const insertionPoint = this.startIndexForPointAtIndex(index);
this.growIfNeeded(pointsRequired);
this.makeInsertionPoint(insertionPoint, pointsRequired);
this.addPoint(
this.makePoint(point, series),
insertionPoint,
pointsRequired
);
this.count += (pointsRequired / 2);
}
makeInsertionPoint(insertionPoint, pointsRequired) {
if (this.count * 2 > insertionPoint) {
if (!this.isTempBuffer) {
this.buffer = Array.prototype.slice.apply(this.buffer);
this.isTempBuffer = true;
}
const target = insertionPoint + pointsRequired;
let start = insertionPoint;
for (; start < target; start++) {
this.buffer.splice(start, 0, 0);
}
}
}
reset() {
this.buffer = new Float32Array(20000);
this.count = 0;
if (this.offset.x) {
this.series.data.forEach(function (point, index) {
this.append(point, index, this.series);
}, this);
}
}
growIfNeeded(pointsRequired) {
const remainingPoints = this.buffer.length - this.count * 2;
let temp;
if (remainingPoints <= pointsRequired) {
temp = new Float32Array(this.buffer.length + 20000);
temp.set(this.buffer);
this.buffer = temp;
}
}
destroy() {
this.stopListening();
}
}

View File

@ -0,0 +1,449 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="gl-plot-chart-area">
<span v-html="canvasTemplate"></span>
<span v-html="canvasTemplate"></span>
</div>
</template>
<script>
import eventHelpers from "../lib/eventHelpers";
import { DrawLoader } from '../draw/DrawLoader';
import MCTChartLineLinear from './MCTChartLineLinear';
import MCTChartLineStepAfter from './MCTChartLineStepAfter';
import MCTChartPointSet from './MCTChartPointSet';
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
import configStore from "../configuration/configStore";
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
const MARKER_SIZE = 6.0;
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
export default {
inject: ['openmct', 'domainObject'],
props: {
rectangles: {
type: Array,
default() {
return [];
}
},
highlights: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
canvasTemplate: '<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>'
};
},
watch: {
highlights() {
this.scheduleDraw();
},
rectangles() {
this.scheduleDraw();
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.isDestroyed = false;
this.lines = [];
this.pointSets = [];
this.alarmSets = [];
this.offset = {};
this.seriesElements = new WeakMap();
let canvasEls = this.$parent.$refs.chartContainer.querySelectorAll("canvas");
const mainCanvas = canvasEls[1];
const overlayCanvas = canvasEls[0];
if (this.initializeCanvas(mainCanvas, overlayCanvas)) {
this.draw();
}
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
this.config.series.forEach(this.onSeriesAdd, this);
},
beforeDestroy() {
this.destroy();
},
methods: {
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (!config) {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct
});
configStore.add(configId, config);
}
return config;
},
reDraw(mode, o, series) {
this.changeInterpolate(mode, o, series);
this.changeMarkers(mode, o, series);
this.changeAlarmMarkers(mode, o, series);
},
onSeriesAdd(series) {
this.listenTo(series, 'change:xKey', this.reDraw, this);
this.listenTo(series, 'change:interpolate', this.changeInterpolate, this);
this.listenTo(series, 'change:markers', this.changeMarkers, this);
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
this.listenTo(series, 'change', this.scheduleDraw);
this.listenTo(series, 'add', this.scheduleDraw);
this.makeChartElement(series);
},
changeInterpolate(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.lines = [];
const newLine = this.lineForSeries(series);
if (newLine) {
elements.lines.push(newLine);
this.lines.push(newLine);
}
},
changeAlarmMarkers(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
},
changeMarkers(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
elements.pointSets = [];
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
},
onSeriesRemove(series) {
this.stopListening(series);
this.removeChartElement(series);
this.scheduleDraw();
},
destroy() {
this.isDestroyed = true;
this.stopListening();
this.lines.forEach(line => line.destroy());
DrawLoader.releaseDrawAPI(this.drawAPI);
},
clearOffset() {
delete this.offset.x;
delete this.offset.y;
delete this.offset.xVal;
delete this.offset.yVal;
delete this.offset.xKey;
delete this.offset.yKey;
this.lines.forEach(function (line) {
line.reset();
});
this.pointSets.forEach(function (pointSet) {
pointSet.reset();
});
},
setOffset(offsetPoint, index, series) {
if (this.offset.x && this.offset.y) {
return;
}
const offsets = {
x: series.getXVal(offsetPoint),
y: series.getYVal(offsetPoint)
};
this.offset.x = function (x) {
return x - offsets.x;
}.bind(this);
this.offset.y = function (y) {
return y - offsets.y;
}.bind(this);
this.offset.xVal = function (point, pSeries) {
return this.offset.x(pSeries.getXVal(point));
}.bind(this);
this.offset.yVal = function (point, pSeries) {
return this.offset.y(pSeries.getYVal(point));
}.bind(this);
},
initializeCanvas(canvas, overlay) {
this.canvas = canvas;
this.overlay = overlay;
this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay);
if (this.drawAPI) {
this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);
}
return Boolean(this.drawAPI);
},
fallbackToCanvas() {
this.stopListening(this.drawAPI);
DrawLoader.releaseDrawAPI(this.drawAPI);
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.TEMPLATE;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
this.canvas = mainCanvas;
this.overlay.parentNode.replaceChild(overlayCanvas, this.overlay);
this.overlay = overlayCanvas;
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$emit('plotReinitializeCanvas');
},
removeChartElement(series) {
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
this.seriesElements.delete(series);
},
lineForSeries(series) {
if (series.get('interpolate') === 'linear') {
return new MCTChartLineLinear(
series,
this,
this.offset
);
}
if (series.get('interpolate') === 'stepAfter') {
return new MCTChartLineStepAfter(
series,
this,
this.offset
);
}
},
pointSetForSeries(series) {
if (series.get('markers')) {
return new MCTChartPointSet(
series,
this,
this.offset
);
}
},
alarmPointSetForSeries(series) {
if (series.get('alarmMarkers')) {
return new MCTChartAlarmPointSet(
series,
this,
this.offset
);
}
},
makeChartElement(series) {
const elements = {
lines: [],
pointSets: []
};
const line = this.lineForSeries(series);
if (line) {
elements.lines.push(line);
this.lines.push(line);
}
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
this.seriesElements.set(series, elements);
},
canDraw() {
if (!this.offset.x || !this.offset.y) {
return false;
}
return true;
},
scheduleDraw() {
if (!this.drawScheduled) {
requestAnimationFrame(this.draw);
this.drawScheduled = true;
}
},
draw() {
this.drawScheduled = false;
if (this.isDestroyed) {
return;
}
this.drawAPI.clear();
if (this.canDraw()) {
this.updateViewport();
this.drawSeries();
this.drawRectangles();
this.drawHighlights();
}
},
updateViewport() {
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
if (!xRange || !yRange) {
return;
}
const dimensions = [
xRange.max - xRange.min,
yRange.max - yRange.min
];
const origin = [
this.offset.x(xRange.min),
this.offset.y(yRange.min)
];
this.drawAPI.setDimensions(
dimensions,
origin
);
},
drawSeries() {
this.lines.forEach(this.drawLine, this);
this.pointSets.forEach(this.drawPoints, this);
this.alarmSets.forEach(this.drawAlarmPoints, this);
},
drawAlarmPoints(alarmSet) {
this.drawAPI.drawLimitPoints(
alarmSet.points,
alarmSet.series.get('color').asRGBAArray(),
alarmSet.series.get('markerSize')
);
},
drawPoints(chartElement) {
this.drawAPI.drawPoints(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
chartElement.series.get('markerSize'),
chartElement.series.get('markerShape')
);
},
drawLine(chartElement) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count
);
},
drawHighlights() {
if (this.highlights && this.highlights.length) {
this.highlights.forEach(this.drawHighlight, this);
}
},
drawHighlight(highlight) {
const points = new Float32Array([
this.offset.xVal(highlight.point, highlight.series),
this.offset.yVal(highlight.point, highlight.series)
]);
const color = highlight.series.get('color').asRGBAArray();
const pointCount = 1;
const shape = highlight.series.get('markerShape');
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
},
drawRectangles() {
if (this.rectangles) {
this.rectangles.forEach(this.drawRectangle, this);
}
},
drawRectangle(rect) {
this.drawAPI.drawSquare(
[
this.offset.x(rect.start.x),
this.offset.y(rect.start.y)
],
[
this.offset.x(rect.end.x),
this.offset.y(rect.end.y)
],
rect.color
);
}
}
};
</script>

View File

@ -0,0 +1,109 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Model from './Model';
export default class Collection extends Model {
initialize(options) {
super.initialize(options);
this.modelClass = Model;
if (options.models) {
this.models = options.models.map(this.modelFn, this);
} else {
this.models = [];
}
}
modelFn(model) {
//TODO: Come back to this - why are we doing this?
if (model instanceof this.modelClass) {
model.collection = this;
return model;
}
return new this.modelClass({
collection: this,
model: model
});
}
first() {
return this.at(0);
}
forEach(iteree, context) {
this.models.forEach(iteree, context);
}
map(iteree, context) {
return this.models.map(iteree, context);
}
filter(iteree, context) {
return this.models.filter(iteree, context);
}
size() {
return this.models.length;
}
at(index) {
return this.models[index];
}
add(model) {
model = this.modelFn(model);
const index = this.models.length;
this.models.push(model);
this.emit('add', model, index);
}
insert(model, index) {
model = this.modelFn(model);
this.models.splice(index, 0, model);
this.emit('add', model, index + 1);
}
indexOf(model) {
return this.models.findIndex(m => m === model);
}
remove(model) {
const index = this.indexOf(model);
if (index === -1) {
throw new Error('model not found in collection.');
}
this.emit('remove', model, index);
this.models.splice(index, 1);
}
destroy(model) {
this.forEach(function (m) {
m.destroy();
});
this.stopListening();
}
}

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Model from "./Model";
/**
* TODO: doc strings.
*/
export default class LegendModel extends Model {
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', this.setHeight, this);
this.listenTo(this.seriesCollection, 'remove', this.setHeight, this);
this.listenTo(this, 'change:expanded', this.setHeight, this);
this.set('expanded', this.get('expandByDefault'));
}
setHeight() {
const expanded = this.get('expanded');
if (this.get('position') !== 'top') {
this.set('height', '0px');
} else {
this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px');
}
}
defaults(options) {
return {
position: 'top',
expandByDefault: false,
hideLegendWhenSmall: false,
valueToShowWhenCollapsed: 'nearestValue',
showTimestampWhenExpanded: true,
showValueWhenExpanded: true,
showMaximumWhenExpanded: true,
showMinimumWhenExpanded: true,
showUnitsWhenExpanded: true
};
}
}

View File

@ -0,0 +1,93 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import eventHelpers from "../lib/eventHelpers";
import _ from 'lodash';
export default class Model extends EventEmitter {
constructor(options) {
super();
//need to do this as we're already extending EventEmitter
eventHelpers.extend(this);
if (!options) {
options = {};
}
this.id = options.id;
this.model = options.model;
this.collection = options.collection;
const defaults = this.defaults(options);
if (!this.model) {
this.model = options.model = defaults;
} else {
_.defaultsDeep(this.model, defaults);
}
this.initialize(options);
this.idAttr = 'id';
}
defaults(options) {
return {};
}
initialize(model) {
}
/**
* Destroy the model, removing all listeners and subscriptions.
*/
destroy() {
this.emit('destroy');
this.removeAllListeners();
}
id() {
return this.get(this.idAttr);
}
get(attribute) {
return this.model[attribute];
}
has(attribute) {
return _.has(this.model, attribute);
}
set(attribute, value) {
const oldValue = this.model[attribute];
this.model[attribute] = value;
this.emit('change', attribute, value, oldValue, this);
this.emit('change:' + attribute, value, oldValue, this);
}
unset(attribute) {
const oldValue = this.model[attribute];
delete this.model[attribute];
this.emit('change', attribute, undefined, oldValue, this);
this.emit('change:' + attribute, undefined, oldValue, this);
}
}

View File

@ -0,0 +1,136 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import _ from 'lodash';
import Model from "./Model";
import SeriesCollection from "./SeriesCollection";
import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The indiidual parts of the plot configuration model
* handle setting defaults and updating in response to various changes.
*
*/
export default class PlotConfigurationModel extends Model {
/**
* Initializes all sub models and then passes references to submodels
* to those that need it.
*/
initialize(options) {
this.openmct = options.openmct;
this.xAxis = new XAxisModel({
model: options.model.xAxis,
plot: this,
openmct: options.openmct
});
this.yAxis = new YAxisModel({
model: options.model.yAxis,
plot: this,
openmct: options.openmct
});
this.legend = new LegendModel({
model: options.model.legend,
plot: this,
openmct: options.openmct
});
this.series = new SeriesCollection({
models: options.model.series,
plot: this,
openmct: options.openmct
});
if (this.get('domainObject').type === 'telemetry.plot.overlay') {
this.removeMutationListener = this.openmct.objects.observe(
this.get('domainObject'),
'*',
this.updateDomainObject.bind(this)
);
}
this.yAxis.listenToSeriesCollection(this.series);
this.legend.listenToSeriesCollection(this.series);
this.listenTo(this, 'destroy', this.onDestroy, this);
}
/**
* Retrieve the persisted series config for a given identifier.
*/
getPersistedSeriesConfig(identifier) {
const domainObject = this.get('domainObject');
if (!domainObject.configuration || !domainObject.configuration.series) {
return;
}
return domainObject.configuration.series.filter(function (seriesConfig) {
return seriesConfig.identifier.key === identifier.key
&& seriesConfig.identifier.namespace === identifier.namespace;
})[0];
}
/**
* Retrieve the persisted filters for a given identifier.
*/
getPersistedFilters(identifier) {
const domainObject = this.get('domainObject');
const keystring = this.openmct.objects.makeKeyString(identifier);
if (!domainObject.configuration || !domainObject.configuration.filters) {
return;
}
return domainObject.configuration.filters[keystring];
}
/**
* Update the domain object with the given value.
*/
updateDomainObject(domainObject) {
this.set('domainObject', domainObject);
}
/**
* Clean up all objects and remove all listeners.
*/
onDestroy() {
this.xAxis.destroy();
this.yAxis.destroy();
this.series.destroy();
this.legend.destroy();
if (this.removeMutationListener) {
this.removeMutationListener();
}
}
/**
* Return defaults, which are extracted from the passed in domain
* object.
*/
defaults(options) {
return {
series: [],
domainObject: options.domainObject,
xAxis: {
},
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
};
}
}

View File

@ -0,0 +1,451 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import _ from 'lodash';
import Model from "./Model";
import { MARKER_SHAPES } from '../draw/MarkerShapes';
/**
* Plot series handle interpreting telemetry metadata for a single telemetry
* object, querying for that data, and formatting it for display purposes.
*
* Plot series emit both collection events and model events:
* `change` when any property changes
* `change:<prop_name>` when a specific property changes.
* `destroy`: when series is destroyed
* `add`: whenever a data point is added to a series
* `remove`: whenever a data point is removed from a series.
* `reset`: whenever the collection is emptied.
*
* Plot series have the following Model properties:
*
* `name`: name of series.
* `identifier`: the Open MCT identifier for the telemetry source for this
* series.
* `xKey`: the telemetry value key for x values fetched from this series.
* `yKey`: the telemetry value key for y values fetched from this series.
* `interpolate`: interpolate method, either `undefined` (no interpolation),
* `linear` (points are connected via straight lines), or
* `stepAfter` (points are connected by steps).
* `markers`: boolean, whether or not this series should render with markers.
* `markerShape`: string, shape of markers.
* `markerSize`: number, size in pixels of markers for this series.
* `alarmMarkers`: whether or not to display alarm markers for this series.
* `stats`: An object that tracks the min and max y values observed in this
* series. This property is checked and updated whenever data is
* added.
*
* Plot series have the following instance properties:
*
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
* telemetry point.
* `formats`: the Open MCT format map for this telemetry point.
*/
export default class PlotSeries extends Model {
constructor(options) {
super(options);
this.data = [];
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
this.persistedConfig = options.persistedConfig;
this.filters = options.filters;
// Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
}
/**
* Set defaults for telemetry series.
*/
defaults(options) {
this.metadata = options
.openmct
.telemetry
.getMetadata(options.domainObject);
this.formats = options
.openmct
.telemetry
.getFormatMap(this.metadata);
const range = this.metadata.valuesForHints(['range'])[0];
return {
name: options.domainObject.name,
unit: range.unit,
xKey: options.collection.plot.xAxis.get('key'),
yKey: range.key,
markers: true,
markerShape: 'point',
markerSize: 2.0,
alarmMarkers: true
};
}
/**
* Remove real-time subscription when destroyed.
*/
onDestroy(model) {
if (this.unsubscribe) {
this.unsubscribe();
}
}
initialize(options) {
this.openmct = options.openmct;
this.domainObject = options.domainObject;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
this.on('destroy', this.onDestroy, this);
}
locateOldObject(oldStyleParent) {
return oldStyleParent.useCapability('composition')
.then(function (children) {
this.oldObject = children
.filter(function (child) {
return child.getId() === this.keyString;
}, this)[0];
}.bind(this));
}
/**
* Fetch historical data and establish a realtime subscription. Returns
* a promise that is resolved when all connections have been successfully
* established.
*
* @returns {Promise}
*/
fetch(options) {
let strategy;
if (this.model.interpolate !== 'none') {
strategy = 'minmax';
}
options = Object.assign({}, {
size: 1000,
strategy,
filters: this.filters
}, options || {});
if (!this.unsubscribe) {
this.unsubscribe = this.openmct
.telemetry
.subscribe(
this.domainObject,
this.add.bind(this),
{
filters: this.filters
}
);
}
/* eslint-disable you-dont-need-lodash-underscore/concat */
return this.openmct
.telemetry
.request(this.domainObject, options)
.then(function (points) {
const newPoints = _(this.data)
.concat(points)
.sortBy(this.getXVal)
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value();
this.reset(newPoints);
}.bind(this));
/* eslint-enable you-dont-need-lodash-underscore/concat */
}
/**
* Update x formatter on x change.
*/
onXKeyChange(xKey) {
const format = this.formats[xKey];
this.getXVal = format.parse.bind(format);
}
/**
* Update y formatter on change, default to stepAfter interpolation if
* y range is an enumeration.
*/
onYKeyChange(newKey, oldKey) {
if (newKey === oldKey) {
return;
}
const valueMetadata = this.metadata.value(newKey);
if (!this.persistedConfig || !this.persistedConfig.interpolate) {
if (valueMetadata.format === 'enum') {
this.set('interpolate', 'stepAfter');
} else {
this.set('interpolate', 'linear');
}
}
this.evaluate = function (datum) {
return this.limitEvaluator.evaluate(datum, valueMetadata);
}.bind(this);
const format = this.formats[newKey];
this.getYVal = format.parse.bind(format);
}
formatX(point) {
return this.formats[this.get('xKey')].format(point);
}
formatY(point) {
return this.formats[this.get('yKey')].format(point);
}
/**
* Clear stats and recalculate from existing data.
*/
resetStats() {
this.unset('stats');
this.data.forEach(this.updateStats, this);
}
/**
* Reset plot series. If new data is provided, will add that
* data to series after reset.
*/
reset(newData) {
this.data = [];
this.resetStats();
this.emit('reset');
if (newData) {
newData.forEach(function (point) {
this.add(point, true);
}, this);
}
}
/**
* Return the point closest to a given x value.
*/
nearestPoint(xValue) {
const insertIndex = this.sortedIndex(xValue);
const lowPoint = this.data[insertIndex - 1];
const highPoint = this.data[insertIndex];
const indexVal = this.getXVal(xValue);
const lowDistance = lowPoint
? indexVal - this.getXVal(lowPoint)
: Number.POSITIVE_INFINITY;
const highDistance = highPoint
? this.getXVal(highPoint) - indexVal
: Number.POSITIVE_INFINITY;
const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint;
return nearestPoint;
}
/**
* Override this to implement plot series loading functionality. Must return
* a promise that is resolved when loading is completed.
*
* @private
* @returns {Promise}
*/
load(options) {
return this.fetch(options)
.then(function (res) {
this.emit('load');
return res;
}.bind(this));
}
/**
* Find the insert index for a given point to maintain sort order.
* @private
*/
sortedIndex(point) {
return _.sortedIndexBy(this.data, point, this.getXVal);
}
/**
* Update min/max stats for the series.
* @private
*/
updateStats(point) {
const value = this.getYVal(point);
let stats = this.get('stats');
let changed = false;
if (!stats) {
stats = {
minValue: value,
minPoint: point,
maxValue: value,
maxPoint: point
};
changed = true;
} else {
if (stats.maxValue < value) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
}
}
if (changed) {
this.set('stats', {
minValue: stats.minValue,
minPoint: stats.minPoint,
maxValue: stats.maxValue,
maxPoint: stats.maxPoint
});
}
}
/**
* Add a point to the data array while maintaining the sort order of
* the array and preventing insertion of points with a duplicate x
* value. Can provide an optional argument to append a point without
* maintaining sort order and dupe checks, which improves performance
* when adding an array of points that are already properly sorted.
*
* @private
* @param {Object} point a telemetry datum.
* @param {Boolean} [appendOnly] default false, if true will append
* a point to the end without dupe checking.
*/
add(point, appendOnly) {
let insertIndex = this.data.length;
const currentYVal = this.getYVal(point);
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
console.warn('[Plot] Invalid Y Values detected');
return;
}
if (!appendOnly) {
insertIndex = this.sortedIndex(point);
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
return;
}
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
return;
}
}
this.updateStats(point);
point.mctLimitState = this.evaluate(point);
this.data.splice(insertIndex, 0, point);
this.emit('add', point, insertIndex, this);
}
/**
*
* @private
*/
isValueInvalid(val) {
return Number.isNaN(val) || val === undefined;
}
/**
* Remove a point from the data array and notify listeners.
* @private
*/
remove(point) {
const index = this.data.indexOf(point);
this.data.splice(index, 1);
this.emit('remove', point, index, this);
}
/**
* Purges records outside a given x range. Changes removal method based
* on number of records to remove: for large purge, reset data and
* rebuild array. for small purge, removes points and emits updates.
*
* @public
* @param {Object} range
* @param {number} range.min minimum x value to keep
* @param {number} range.max maximum x value to keep.
*/
purgeRecordsOutsideRange(range) {
const startIndex = this.sortedIndex(range.min);
const endIndex = this.sortedIndex(range.max) + 1;
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
if (pointsToRemove > 0) {
if (pointsToRemove < 1000) {
this.data.slice(0, startIndex).forEach(this.remove, this);
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
this.resetStats();
} else {
const newData = this.data.slice(startIndex, endIndex);
this.reset(newData);
}
}
}
/**
* Updates filters, clears the plot series, unsubscribes and resubscribes
* @public
*/
updateFiltersAndRefresh(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.reset();
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.fetch();
} else {
this.filters = deepCopiedFilters;
}
}
getDisplayRange(xKey) {
const unsortedData = this.data;
this.data = [];
unsortedData.forEach(point => this.add(point, false));
const minValue = this.getXVal(this.data[0]);
const maxValue = this.getXVal(this.data[this.data.length - 1]);
return {
min: minValue,
max: maxValue
};
}
markerOptionsDisplayText() {
const showMarkers = this.get('markers');
if (!showMarkers) {
return "Disabled";
}
const markerShapeKey = this.get('markerShape');
const markerShape = MARKER_SHAPES[markerShapeKey].label;
const markerSize = this.get('markerSize');
return `${markerShape}: ${markerSize}px`;
}
nameWithUnit() {
let unit = this.get('unit');
return this.get('name') + (unit ? ' ' + unit : '');
}
}

View File

@ -0,0 +1,165 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import _ from 'lodash';
import PlotSeries from "./PlotSeries";
import Collection from "./Collection";
import Color from "../lib/Color";
import ColorPalette from "../lib/ColorPalette";
export default class SeriesCollection extends Collection {
initialize(options) {
super.initialize(options);
this.modelClass = PlotSeries;
this.plot = options.plot;
this.openmct = options.openmct;
this.palette = new ColorPalette();
this.listenTo(this, 'add', this.onSeriesAdd, this);
this.listenTo(this, 'remove', this.onSeriesRemove, this);
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
const domainObject = this.plot.get('domainObject');
if (domainObject.telemetry) {
this.addTelemetryObject(domainObject);
} else {
this.watchTelemetryContainer(domainObject);
}
}
trackPersistedConfig(domainObject) {
domainObject.configuration.series.forEach(function (seriesConfig) {
const series = this.byIdentifier(seriesConfig.identifier);
if (series) {
series.persistedConfig = seriesConfig;
}
}, this);
}
watchTelemetryContainer(domainObject) {
const composition = this.openmct.composition.get(domainObject);
this.listenTo(composition, 'add', this.addTelemetryObject, this);
this.listenTo(composition, 'remove', this.removeTelemetryObject, this);
composition.load();
}
addTelemetryObject(domainObject, index) {
let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier);
const filters = this.plot.getPersistedFilters(domainObject.identifier);
const plotObject = this.plot.get('domainObject');
if (!seriesConfig) {
seriesConfig = {
identifier: domainObject.identifier
};
if (plotObject.type === 'telemetry.plot.overlay') {
this.openmct.objects.mutate(
plotObject,
'configuration.series[' + this.size() + ']',
seriesConfig
);
seriesConfig = this.plot
.getPersistedSeriesConfig(domainObject.identifier);
}
}
// Clone to prevent accidental mutation by ref.
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
this.add(new PlotSeries({
model: seriesConfig,
domainObject: domainObject,
collection: this,
openmct: this.openmct,
persistedConfig: this.plot
.getPersistedSeriesConfig(domainObject.identifier),
filters: filters
}));
}
removeTelemetryObject(identifier) {
const plotObject = this.plot.get('domainObject');
if (plotObject.type === 'telemetry.plot.overlay') {
const persistedIndex = plotObject.configuration.series.findIndex(s => {
return _.isEqual(identifier, s.identifier);
});
const configIndex = this.models.findIndex(m => {
return _.isEqual(m.domainObject.identifier, identifier);
});
/*
when cancelling out of edit mode, the config store and domain object are out of sync
thus it is necesarry to check both and remove the models that are no longer in composition
*/
if (persistedIndex === -1) {
this.remove(this.at(configIndex));
} else {
this.remove(this.at(persistedIndex));
// Because this is triggered by a composition change, we have
// to defer mutation of our plot object, otherwise we might
// mutate an outdated version of the plotObject.
setTimeout(function () {
const newPlotObject = this.plot.get('domainObject');
const cSeries = newPlotObject.configuration.series.slice();
cSeries.splice(persistedIndex, 1);
this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries);
}.bind(this));
}
}
}
onSeriesAdd(series) {
let seriesColor = series.get('color');
if (seriesColor) {
if (!(seriesColor instanceof Color)) {
seriesColor = Color.fromHexString(seriesColor);
series.set('color', seriesColor);
}
this.palette.remove(seriesColor);
} else {
series.set('color', this.palette.getNextColor());
}
this.listenTo(series, 'change:color', this.updateColorPalette, this);
}
onSeriesRemove(series) {
this.palette.return(series.get('color'));
this.stopListening(series);
series.destroy();
}
updateColorPalette(newColor, oldColor) {
this.palette.remove(newColor);
const seriesWithColor = this.filter(function (series) {
return series.get('color') === newColor;
})[0];
if (!seriesWithColor) {
this.palette.return(oldColor);
}
}
byIdentifier(identifier) {
return this.filter(function (series) {
const seriesIdentifier = series.get('identifier');
return seriesIdentifier.namespace === identifier.namespace
&& seriesIdentifier.key === identifier.key;
})[0];
}
}

View File

@ -0,0 +1,88 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Model from "./Model";
/**
* TODO: doc strings.
*/
export default class XAxisModel extends Model {
initialize(options) {
this.plot = options.plot;
this.set('label', options.model.name || '');
this.on('change:range', function (newValue, oldValue, model) {
if (!model.get('frozen')) {
model.set('displayRange', newValue);
}
});
this.on('change:frozen', ((frozen, oldValue, model) => {
if (!frozen) {
model.set('range', this.get('range'));
}
}));
if (this.get('range')) {
this.set('range', this.get('range'));
}
this.listenTo(this, 'change:key', this.changeKey, this);
}
changeKey(newKey) {
const series = this.plot.series.first();
if (series) {
const xMetadata = series.metadata.value(newKey);
const xFormat = series.formats[newKey];
this.set('label', xMetadata.name);
this.set('format', xFormat.format.bind(xFormat));
} else {
this.set('format', function (x) {
return x;
});
this.set('label', newKey);
}
this.plot.series.forEach(function (plotSeries) {
plotSeries.set('xKey', newKey);
});
}
resetSeries() {
this.plot.series.forEach(function (plotSeries) {
plotSeries.reset();
});
}
defaults(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.$injector.get('formatService')
.getFormat(timeSystem.timeFormat);
return {
name: timeSystem.name,
key: timeSystem.key,
format: format.format.bind(format),
range: {
min: bounds.start,
max: bounds.end
},
frozen: false
};
}
}

View File

@ -0,0 +1,235 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import _ from 'lodash';
import Model from './Model';
/**
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the x Axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
*/
export default class YAxisModel extends Model {
initialize(options) {
this.plot = options.plot;
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
this.updateDisplayRange(this.get('range'));
}
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', (series => {
this.trackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
this.listenTo(this.seriesCollection, 'remove', (series => {
this.untrackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
this.seriesCollection.forEach(this.trackSeries, this);
this.updateFromSeries(this.seriesCollection);
}
updateDisplayRange(range) {
if (!this.get('autoscale')) {
this.set('displayRange', range);
}
}
toggleFreeze(frozen) {
if (!frozen) {
this.toggleAutoscale(this.get('autoscale'));
}
}
applyPadding(range) {
let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding');
if (padding === 0) {
padding = 1;
}
return {
min: range.min - padding,
max: range.max + padding
};
}
updatePadding(newPadding) {
if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
}
}
calculateAutoscaleExtents(newStats) {
if (this.get('autoscale') && !this.get('frozen')) {
if (!newStats) {
this.unset('displayRange');
} else {
this.set('displayRange', this.applyPadding(newStats));
}
}
}
updateStats(seriesStats) {
if (!this.has('stats')) {
this.set('stats', {
min: seriesStats.minValue,
max: seriesStats.maxValue
});
return;
}
const stats = this.get('stats');
let changed = false;
if (stats.min > seriesStats.minValue) {
changed = true;
stats.min = seriesStats.minValue;
}
if (stats.max < seriesStats.maxValue) {
changed = true;
stats.max = seriesStats.maxValue;
}
if (changed) {
this.set('stats', {
min: stats.min,
max: stats.max
});
}
}
resetStats() {
this.unset('stats');
this.seriesCollection.forEach(function (series) {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
}, this);
}
trackSeries(series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (!seriesStats) {
this.resetStats();
} else {
this.updateStats(seriesStats);
}
});
this.listenTo(series, 'change:yKey', () => {
this.updateFromSeries(this.seriesCollection);
});
}
untrackSeries(series) {
this.stopListening(series);
this.resetStats();
this.updateFromSeries(this.seriesCollection);
}
toggleAutoscale(autoscale) {
if (autoscale && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
} else {
this.set('displayRange', this.get('range'));
}
}
/**
* Update yAxis format, values, and label from known series.
*/
updateFromSeries(series) {
this.unset('displayRange');
const plotModel = this.plot.get('domainObject');
const label = _.get(plotModel, 'configuration.yAxis.label');
const sampleSeries = series.first();
if (!sampleSeries) {
if (!label) {
this.unset('label');
}
return;
}
const yKey = sampleSeries.get('yKey');
const yMetadata = sampleSeries.metadata.value(yKey);
const yFormat = sampleSeries.formats[yKey];
this.set('format', yFormat.format.bind(yFormat));
this.set('values', yMetadata.values);
if (!label) {
const labelName = series.map(function (s) {
return s.metadata.value(s.get('yKey')).name;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelName) {
this.set('label', labelName);
return;
}
const labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelUnits) {
this.set('label', labelUnits);
return;
}
}
}
defaults(options) {
return {
frozen: false,
autoscale: true,
autoscalePadding: 0.1
};
}
}

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
function ConfigStore() {
this.store = {};
}
ConfigStore.prototype.deleteStore = function (id) {
if (this.store[id]) {
this.store[id].destroy();
delete this.store[id];
}
};
ConfigStore.prototype.deleteAll = function () {
Object.keys(this.store).forEach(id => this.deleteStore(id));
};
ConfigStore.prototype.add = function (id, config) {
this.store[id] = config;
};
ConfigStore.prototype.get = function (id) {
return this.store[id];
};
const STORE = new ConfigStore();
export default STORE;

View File

@ -0,0 +1,163 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import eventHelpers from '../lib/eventHelpers';
import { MARKER_SHAPES } from './MarkerShapes';
/**
* Create a new draw API utilizing the Canvas's 2D API for rendering.
*
* @constructor
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if Canvas's 2D API is unavailab
*/
/**
* Create a new draw API utilizing the Canvas's 2D API for rendering.
*
* @constructor
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if Canvas's 2D API is unavailab
*/
function Draw2D(canvas) {
this.canvas = canvas;
this.c2d = canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.dimensions = [this.width, this.height];
this.origin = [0, 0];
if (!this.c2d) {
throw new Error("Canvas 2d API unavailable.");
}
}
Object.assign(Draw2D.prototype, EventEmitter.prototype);
eventHelpers.extend(Draw2D.prototype);
// Convert from logical to physical x coordinates
Draw2D.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
};
// Convert from logical to physical y coordinates
Draw2D.prototype.y = function (v) {
return this.height
- ((v - this.origin[1]) / this.dimensions[1]) * this.height;
};
// Set the color to be used for drawing operations
Draw2D.prototype.setColor = function (color) {
const mappedColor = color.map(function (c, i) {
return i < 3 ? Math.floor(c * 255) : (c);
}).join(',');
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
};
Draw2D.prototype.clear = function () {
this.width = this.canvas.width = this.canvas.offsetWidth;
this.height = this.canvas.height = this.canvas.offsetHeight;
this.c2d.clearRect(0, 0, this.width, this.height);
};
Draw2D.prototype.setDimensions = function (newDimensions, newOrigin) {
this.dimensions = newDimensions;
this.origin = newOrigin;
};
Draw2D.prototype.drawLine = function (buf, color, points) {
let i;
this.setColor(color);
// Configure context to draw two-pixel-thick lines
this.c2d.lineWidth = 1;
// Start a new path...
if (buf.length > 1) {
this.c2d.beginPath();
this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));
}
// ...and add points to it...
for (i = 2; i < points * 2; i = i + 2) {
this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));
}
// ...before finally drawing it.
this.c2d.stroke();
};
Draw2D.prototype.drawSquare = function (min, max, color) {
const x1 = this.x(min[0]);
const y1 = this.y(min[1]);
const w = this.x(max[0]) - x1;
const h = this.y(max[1]) - y1;
this.setColor(color);
this.c2d.fillRect(x1, y1, w, h);
};
Draw2D.prototype.drawPoints = function (
buf,
color,
points,
pointSize,
shape
) {
const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this);
this.setColor(color);
for (let i = 0; i < points; i++) {
drawC2DShape(
this.x(buf[i * 2]),
this.y(buf[i * 2 + 1]),
pointSize
);
}
};
Draw2D.prototype.drawLimitPoint = function (x, y, size) {
this.c2d.fillRect(x + size, y, size, size);
this.c2d.fillRect(x, y + size, size, size);
this.c2d.fillRect(x - size, y, size, size);
this.c2d.fillRect(x, y - size, size, size);
};
Draw2D.prototype.drawLimitPoints = function (points, color, pointSize) {
const limitSize = pointSize * 2;
const offset = limitSize / 2;
this.setColor(color);
for (let i = 0; i < points.length; i++) {
this.drawLimitPoint(
this.x(points[i].x) - offset,
this.y(points[i].y) - offset,
limitSize
);
}
};
export default Draw2D;

View File

@ -0,0 +1,102 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import DrawWebGL from './DrawWebGL';
import Draw2D from './Draw2D';
const CHARTS = [
{
MAX_INSTANCES: 16,
API: DrawWebGL,
ALLOCATIONS: []
},
{
MAX_INSTANCES: Number.POSITIVE_INFINITY,
API: Draw2D,
ALLOCATIONS: []
}
];
/**
* Draw loader attaches a draw API to a canvas element and returns the
* draw API.
*/
export const DrawLoader = {
/**
* Return the first draw API available. Returns
* `undefined` if a draw API could not be constructed.
*.
* @param {CanvasElement} canvas - The canvas eelement to attach
the draw API to.
*/
getDrawAPI: function (canvas, overlay) {
let api;
CHARTS.forEach(function (CHART_TYPE) {
if (api) {
return;
}
if (CHART_TYPE.ALLOCATIONS.length
>= CHART_TYPE.MAX_INSTANCES) {
return;
}
try {
api = new CHART_TYPE.API(canvas, overlay);
CHART_TYPE.ALLOCATIONS.push(api);
} catch (e) {
console.warn([
"Could not instantiate chart",
CHART_TYPE.API.name,
";",
e.message
].join(" "));
}
});
if (!api) {
console.warn("Cannot initialize mct-chart.");
}
return api;
},
/**
* Returns a fallback draw api.
*/
getFallbackDrawAPI: function (canvas, overlay) {
const api = new CHARTS[1].API(canvas, overlay);
CHARTS[1].ALLOCATIONS.push(api);
return api;
},
releaseDrawAPI: function (api) {
CHARTS.forEach(function (CHART_TYPE) {
if (api instanceof CHART_TYPE.API) {
CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1);
}
});
if (api.destroy) {
api.destroy();
}
}
};

View File

@ -0,0 +1,300 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import eventHelpers from '../lib/eventHelpers';
import { MARKER_SHAPES } from './MarkerShapes';
// WebGL shader sources (for drawing plain colors)
const FRAGMENT_SHADER = `
precision mediump float;
uniform vec4 uColor;
uniform int uMarkerShape;
void main(void) {
gl_FragColor = uColor;
if (uMarkerShape > 1) {
vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;
if (uMarkerShape == 2) { // circle
float distance = length(clipSpacePointCoord);
if (distance > 1.0) {
discard;
}
} else if (uMarkerShape == 3) { // diamond
float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);
if (distance > 1.0) {
discard;
}
} else if (uMarkerShape == 4) { // triangle
float x = clipSpacePointCoord.x;
float y = clipSpacePointCoord.y;
float distance = 2.0 * x - 1.0;
float distance2 = -2.0 * x - 1.0;
if (distance > y || distance2 > y) {
discard;
}
}
}
}
`;
const VERTEX_SHADER = `
attribute vec2 aVertexPosition;
uniform vec2 uDimensions;
uniform vec2 uOrigin;
uniform float uPointSize;
void main(void) {
gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
gl_PointSize = uPointSize;
}
`;
/**
* Create a draw api utilizing WebGL.
*
* @constructor
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if WebGL is unavailable.
*/
function DrawWebGL(canvas, overlay) {
this.canvas = canvas;
this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true })
|| this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true });
this.overlay = overlay;
this.c2d = overlay.getContext('2d');
if (!this.c2d) {
throw new Error("No canvas 2d!");
}
// Ensure a context was actually available before proceeding
if (!this.gl) {
throw new Error("WebGL unavailable.");
}
this.initContext();
this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this);
}
Object.assign(DrawWebGL.prototype, EventEmitter.prototype);
eventHelpers.extend(DrawWebGL.prototype);
DrawWebGL.prototype.onContextLost = function (event) {
this.emit('error');
this.isContextLost = true;
this.destroy();
};
DrawWebGL.prototype.initContext = function () {
// Initialize shaders
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
this.gl.compileShader(this.vertexShader);
this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
this.gl.compileShader(this.fragmentShader);
// Assemble vertex/fragment shaders into programs
this.program = this.gl.createProgram();
this.gl.attachShader(this.program, this.vertexShader);
this.gl.attachShader(this.program, this.fragmentShader);
this.gl.linkProgram(this.program);
this.gl.useProgram(this.program);
// Get locations for attribs/uniforms from the
// shader programs (to pass values into shaders at draw-time)
this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition");
this.uColor = this.gl.getUniformLocation(this.program, "uColor");
this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape");
this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions");
this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin");
this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize");
this.gl.enableVertexAttribArray(this.aVertexPosition);
// Create a buffer to holds points which will be drawn
this.buffer = this.gl.createBuffer();
// Enable blending, for smoothness
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
};
DrawWebGL.prototype.destroy = function () {
this.stopListening();
};
// Convert from logical to physical x coordinates
DrawWebGL.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
};
// Convert from logical to physical y coordinates
DrawWebGL.prototype.y = function (v) {
return this.height
- ((v - this.origin[1]) / this.dimensions[1]) * this.height;
};
DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
if (this.isContextLost) {
return;
}
const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.uniform4fv(this.uColor, color);
this.gl.uniform1i(this.uMarkerShape, shapeCode);
this.gl.drawArrays(drawType, 0, points);
};
DrawWebGL.prototype.clear = function () {
if (this.isContextLost) {
return;
}
this.height = this.canvas.height = this.canvas.offsetHeight;
this.width = this.canvas.width = this.canvas.offsetWidth;
this.overlay.height = this.overlay.offsetHeight;
this.overlay.width = this.overlay.offsetWidth;
// Set the viewport size; note that we use the width/height
// that our WebGL context reports, which may be lower
// resolution than the canvas we requested.
this.gl.viewport(
0,
0,
this.gl.drawingBufferWidth,
this.gl.drawingBufferHeight
);
this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.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
*/
DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
this.dimensions = dimensions;
this.origin = origin;
if (this.isContextLost) {
return;
}
if (dimensions && dimensions.length > 0
&& origin && origin.length > 0) {
this.gl.uniform2fv(this.uDimensions, dimensions);
this.gl.uniform2fv(this.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
*/
DrawWebGL.prototype.drawLine = function (buf, color, points) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
};
/**
* Draw the buffer as points.
*
*/
DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) {
if (this.isContextLost) {
return;
}
this.gl.uniform1f(this.uPointSize, pointSize);
this.doDraw(this.gl.POINTS, buf, color, points, shape);
};
/**
* 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
*/
DrawWebGL.prototype.drawSquare = function (min, max, color) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
), color, 4);
};
DrawWebGL.prototype.drawLimitPoint = function (x, y, size) {
this.c2d.fillRect(x + size, y, size, size);
this.c2d.fillRect(x, y + size, size, size);
this.c2d.fillRect(x - size, y, size, size);
this.c2d.fillRect(x, y - size, size, size);
};
DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) {
const limitSize = pointSize * 2;
const offset = limitSize / 2;
const mappedColor = color.map(function (c, i) {
return i < 3 ? Math.floor(c * 255) : (c);
}).join(',');
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
for (let i = 0; i < points.length; i++) {
this.drawLimitPoint(
this.x(points[i].x) - offset,
this.y(points[i].y) - offset,
limitSize
);
}
};
export default DrawWebGL;

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* @label string (required) display name of shape
* @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader
* @drawC2D function (required) canvas2d draw function
*/
export const MARKER_SHAPES = {
point: {
label: 'Point',
drawWebGL: 1,
drawC2D: function (x, y, size) {
const offset = size / 2;
this.c2d.fillRect(x - offset, y - offset, size, size);
}
},
circle: {
label: 'Circle',
drawWebGL: 2,
drawC2D: function (x, y, size) {
const radius = size / 2;
this.c2d.beginPath();
this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false);
this.c2d.closePath();
this.c2d.fill();
}
},
diamond: {
label: 'Diamond',
drawWebGL: 3,
drawC2D: function (x, y, size) {
const offset = size / 2;
const top = [x, y + offset];
const right = [x + offset, y];
const bottom = [x, y - offset];
const left = [x - offset, y];
this.c2d.beginPath();
this.c2d.moveTo(...top);
this.c2d.lineTo(...right);
this.c2d.lineTo(...bottom);
this.c2d.lineTo(...left);
this.c2d.closePath();
this.c2d.fill();
}
},
triangle: {
label: 'Triangle',
drawWebGL: 4,
drawC2D: function (x, y, size) {
const offset = size / 2;
const v1 = [x, y - offset];
const v2 = [x - offset, y + offset];
const v3 = [x + offset, y + offset];
this.c2d.beginPath();
this.c2d.moveTo(...v1);
this.c2d.lineTo(...v2);
this.c2d.lineTo(...v3);
this.c2d.closePath();
this.c2d.fill();
}
}
};

View File

@ -0,0 +1,153 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="c-plot-legend gl-plot-legend"
:class="{
'hover-on-plot': !!highlights.length,
'is-legend-hidden': isLegendHidden
}"
>
<div class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
:class="{ 'c-disclosure-triangle--expanded': isLegendExpanded }"
@click="expandLegend"
>
</div>
<div class="c-plot-legend__wrapper"
:class="{ 'is-cursor-locked': cursorLocked }"
>
<!-- COLLAPSED PLOT LEGEND -->
<div class="plot-wrapper-collapsed-legend"
:class="{'is-cursor-locked': cursorLocked }"
>
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock"
title="Cursor is point locked. Click anywhere in the plot to unlock."
></div>
<plot-legend-item-collapsed v-for="seriesObject in series"
:key="seriesObject.keyString"
:highlights="highlights"
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
:series-object="seriesObject"
:closest="seriesObject.closest"
/>
</div>
<!-- EXPANDED PLOT LEGEND -->
<div class="plot-wrapper-expanded-legend"
:class="{'is-cursor-locked': cursorLocked }"
>
<div class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock"
title="Click anywhere in the plot to unlock."
> Cursor locked to point</div>
<table>
<thead>
<tr>
<th>Name</th>
<th v-if="showTimestampWhenExpanded">
Timestamp
</th>
<th v-if="showValueWhenExpanded">
Value
</th>
<th v-if="showUnitsWhenExpanded">
Unit
</th>
<th v-if="showMinimumWhenExpanded"
class="mobile-hide"
>
Min
</th>
<th v-if="showMaximumWhenExpanded"
class="mobile-hide"
>
Max
</th>
</tr>
</thead>
<tbody>
<plot-legend-item-expanded v-for="seriesObject in series"
:key="seriesObject.keyString"
:series-object="seriesObject"
:highlights="highlights"
:legend="legend"
/>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import PlotLegendItemCollapsed from "./PlotLegendItemCollapsed.vue";
import PlotLegendItemExpanded from "./PlotLegendItemExpanded.vue";
export default {
components: {
PlotLegendItemExpanded,
PlotLegendItemCollapsed
},
inject: ['openmct', 'domainObject'],
props: {
cursorLocked: {
type: Boolean,
default() {
return false;
}
},
series: {
type: Array,
default() {
return [];
}
},
highlights: {
type: Array,
default() {
return [];
}
},
legend: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isLegendHidden: this.legend.get('hideLegendWhenSmall') !== true,
isLegendExpanded: this.legend.get('expanded') === true,
showTimestampWhenExpanded: this.legend.get('showTimestampWhenExpanded') === true,
showValueWhenExpanded: this.legend.get('showValueWhenExpanded') === true,
showUnitsWhenExpanded: this.legend.get('showUnitsWhenExpanded') === true,
showMinimumWhenExpanded: this.legend.get('showMinimumWhenExpanded') === true,
showMaximumWhenExpanded: this.legend.get('showMaximumWhenExpanded') === true
};
},
methods: {
expandLegend() {
this.isLegendExpanded = !this.isLegendExpanded;
this.legend.set('expanded', this.isLegendExpanded);
}
}
};
</script>

View File

@ -0,0 +1,128 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="plot-legend-item"
:class="{
'is-status--missing': isMissing
}"
>
<div class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
:style="{ 'background-color': colorAsHexString }"
>
</span>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
<span class="plot-series-name">{{ nameWithUnit }}</span>
</div>
<div v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none')"
class="plot-series-value hover-value-enabled"
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
>
<span v-if="valueToShowWhenCollapsed === 'nearestValue'">{{ formattedYValue }}</span>
<span v-else-if="valueToShowWhenCollapsed === 'nearestTimestamp'">{{ formattedXValue }}</span>
<span v-else>{{ formattedYValueFromStats }}</span>
</div>
</div>
</template>
<script>
export default {
props: {
valueToShowWhenCollapsed: {
type: String,
required: true
},
seriesObject: {
type: Object,
required: true,
default() {
return {};
}
},
highlights: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
isMissing: false,
colorAsHexString: '',
nameWithUnit: '',
formattedYValue: '',
formattedXValue: '',
mctLimitStateClass: '',
formattedYValueFromStats: ''
};
},
computed: {
valueToDisplayWhenCollapsedClass() {
return `value-to-display-${ this.valueToShowWhenCollapsed }`;
},
notNearest() {
return (this.valueToShowWhenCollapsed.indexOf('nearest') > -1);
}
},
watch: {
highlights(newHighlights) {
const highlightedObject = newHighlights.find(highlight => highlight.series.keyString === this.seriesObject.keyString);
if (newHighlights.length === 0 || highlightedObject) {
this.initialize(highlightedObject);
}
}
},
mounted() {
this.initialize();
},
methods: {
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();
this.nameWithUnit = seriesObject.nameWithUnit();
const closest = seriesObject.closest;
if (closest) {
this.formattedYValue = seriesObject.formatY(closest);
this.formattedXValue = seriesObject.formatX(closest);
this.mctLimitStateClass = closest.mctLimitState ? `${closest.mctLimitState.cssClass}` : '';
} else {
this.formattedYValue = '';
this.formattedXValue = '';
this.mctLimitStateClass = '';
}
const stats = seriesObject.get('stats');
if (stats) {
this.formattedYValueFromStats = seriesObject.formatY(stats[this.valueToShowWhenCollapsed + 'Point']);
} else {
this.formattedYValueFromStats = '';
}
}
}
};
</script>

View File

@ -0,0 +1,154 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<tr
class="plot-legend-item"
:class="{
'is-status--missing': isMissing
}"
>
<td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
:style="{ 'background-color': colorAsHexString }"
>
</span>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
<span class="plot-series-name">{{ name }}</span>
</td>
<td v-if="showTimestampWhenExpanded">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ formattedXValue }}
</span>
</td>
<td v-if="showValueWhenExpanded">
<span class="plot-series-value cursor-hover hover-value-enabled"
:class="[mctLimitStateClass]"
>
{{ formattedYValue }}
</span>
</td>
<td v-if="showUnitsWhenExpanded">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ unit }}
</span>
</td>
<td v-if="showMinimumWhenExpanded"
class="mobile-hide"
>
<span class="plot-series-value">
{{ formattedMinY }}
</span>
</td>
<td v-if="showMaximumWhenExpanded"
class="mobile-hide"
>
<span class="plot-series-value">
{{ formattedMaxY }}
</span>
</td>
</tr>
</template>
<script>
export default {
props: {
seriesObject: {
type: Object,
required: true,
default() {
return {};
}
},
highlights: {
type: Array,
default() {
return [];
}
},
legend: {
type: Object,
required: true
}
},
data() {
return {
showTimestampWhenExpanded: this.legend.get('showTimestampWhenExpanded'),
showValueWhenExpanded: this.legend.get('showValueWhenExpanded'),
showUnitsWhenExpanded: this.legend.get('showUnitsWhenExpanded'),
showMinimumWhenExpanded: this.legend.get('showMinimumWhenExpanded'),
showMaximumWhenExpanded: this.legend.get('showMaximumWhenExpanded'),
isMissing: false,
colorAsHexString: '',
name: '',
unit: '',
formattedYValue: '',
formattedXValue: '',
formattedMinY: '',
formattedMaxY: '',
mctLimitStateClass: ''
};
},
watch: {
highlights(newHighlights) {
const highlightedObject = newHighlights.find(highlight => highlight.series.keyString === this.seriesObject.keyString);
if (newHighlights.length === 0 || highlightedObject) {
this.initialize(highlightedObject);
}
}
},
mounted() {
this.initialize();
},
methods: {
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();
this.name = seriesObject.get('name');
this.unit = seriesObject.get('unit');
const closest = seriesObject.closest;
if (closest) {
this.formattedYValue = seriesObject.formatY(closest);
this.formattedXValue = seriesObject.formatX(closest);
this.mctLimitStateClass = seriesObject.closest.mctLimitState ? seriesObject.closest.mctLimitState.cssClass : '';
} else {
this.formattedYValue = '';
this.formattedXValue = '';
this.mctLimitStateClass = '';
}
const stats = seriesObject.get('stats');
if (stats) {
this.formattedMinY = seriesObject.formatY(stats.minPoint);
this.formattedMaxY = seriesObject.formatY(stats.maxPoint);
} else {
this.formattedMinY = '';
this.formattedMaxY = '';
}
}
}
};
</script>

View File

@ -0,0 +1,98 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* A representation of a color that allows conversions between different
* formats.
*
* @constructor
*/
/**
* A representation of a color that allows conversions between different
* formats.
*
* @constructor
*/
function Color(integerArray) {
this.integerArray = integerArray;
}
Color.fromHexString = function (hexString) {
if (!/#([0-9a-fA-F]{2}){2}/.test(hexString)) {
throw new Error(
'Invalid input "'
+ hexString
+ '". Hex string must be in CSS format e.g. #00FF00'
);
}
return new Color([
parseInt(hexString.slice(1, 3), 16),
parseInt(hexString.slice(3, 5), 16),
parseInt(hexString.slice(5, 7), 16)
]);
};
/**
* Return color as a three element array of RGB values, where each value
* is a integer in the range of 0-255.
*
* @return {number[]} the color, as integer RGB values
*/
Color.prototype.asIntegerArray = function () {
return this.integerArray.map(function (c) {
return c;
});
};
/**
* Return color as a string using #-prefixed six-digit RGB hex notation
* (e.g. #FF0000). See http://www.w3.org/TR/css3-color/#rgb-color.
*
* @return {string} the color, as a style-friendly string
*/
Color.prototype.asHexString = function () {
return '#' + this.integerArray.map(function (c) {
return (c < 16 ? '0' : '') + c.toString(16);
}).join('');
};
/**
* Return color as a RGBA float array.
*
* This format is present specifically to support use with
* WebGL, which expects colors of that form.
*
* @return {number[]} the color, as floating-point RGBA values
*/
Color.prototype.asRGBAArray = function () {
return this.integerArray.map(function (c) {
return c / 255.0;
}).concat([1]);
};
Color.prototype.equalTo = function (otherColor) {
return this.asHexString() === otherColor.asHexString();
};
export default Color;

View File

@ -0,0 +1,62 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
export const COLOR_PALETTE = [
[0x20, 0xB2, 0xAA],
[0x9A, 0xCD, 0x32],
[0xFF, 0x8C, 0x00],
[0xD2, 0xB4, 0x8C],
[0x40, 0xE0, 0xD0],
[0x41, 0x69, 0xFF],
[0xFF, 0xD7, 0x00],
[0x6A, 0x5A, 0xCD],
[0xEE, 0x82, 0xEE],
[0xCC, 0x99, 0x66],
[0x99, 0xCC, 0xCC],
[0x66, 0xCC, 0x33],
[0xFF, 0xCC, 0x00],
[0xFF, 0x66, 0x33],
[0xCC, 0x66, 0xFF],
[0xFF, 0x00, 0x66],
[0xFF, 0xFF, 0x00],
[0x80, 0x00, 0x80],
[0x00, 0x86, 0x8B],
[0x00, 0x8A, 0x00],
[0xFF, 0x00, 0x00],
[0x00, 0x00, 0xFF],
[0xF5, 0xDE, 0xB3],
[0xBC, 0x8F, 0x8F],
[0x46, 0x82, 0xB4],
[0xFF, 0xAF, 0xAF],
[0x43, 0xCD, 0x80],
[0xCD, 0xC1, 0xC5],
[0xA0, 0x52, 0x2D],
[0x64, 0x95, 0xED]
];
export function isDefaultColor(color) {
const a = color.asIntegerArray();
return COLOR_PALETTE.some(function (b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
});
}

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* A color palette stores a set of colors and allows for different
* methods of color allocation.
*
* @constructor
*/
import { isDefaultColor, COLOR_PALETTE } from './ColorHelper';
import Color from './Color';
/**
* A color palette stores a set of colors and allows for different
* methods of color allocation.
*
* @constructor
*/
function ColorPalette() {
const allColors = this.allColors = COLOR_PALETTE.map(function (color) {
return new Color(color);
});
this.colorGroups = [[], [], []];
for (let i = 0; i < allColors.length; i++) {
this.colorGroups[i % 3].push(allColors[i]);
}
this.reset();
}
/**
*
*/
ColorPalette.prototype.groups = function () {
return this.colorGroups;
};
ColorPalette.prototype.reset = function () {
this.availableColors = this.allColors.slice();
};
ColorPalette.prototype.remove = function (color) {
this.availableColors = this.availableColors.filter(function (c) {
return !c.equalTo(color);
});
};
ColorPalette.prototype.return = function (color) {
if (isDefaultColor(color)) {
this.availableColors.unshift(color);
}
};
ColorPalette.prototype.getByHexString = function (hexString) {
const color = Color.fromHexString(hexString);
return color;
};
/**
* @returns {Color} the next unused color in the palette. If all colors
* have been allocated, it will wrap around.
*/
ColorPalette.prototype.getNextColor = function () {
if (!this.availableColors.length) {
console.warn('Color Palette empty, reusing colors!');
this.reset();
}
return this.availableColors.shift();
};
/**
* @param {number} index the index of the color to return. An index
* value larger than the size of the index will wrap around.
* @returns {Color}
*/
ColorPalette.prototype.getColor = function (index) {
return this.colors[index % this.colors.length];
};
export default ColorPalette;

View File

@ -0,0 +1,92 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/*jscs:disable disallowDanglingUnderscores */
const helperFunctions = {
listenTo: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
const listener = {
object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
}
};
export default helperFunctions;

View File

@ -0,0 +1,34 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import PlotViewProvider from './PlotViewProvider';
import OverlayPlotViewProvider from '../overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from '../stackedPlot/StackedPlotViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
};
}

View File

@ -0,0 +1,562 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "./plugin";
import Vue from "vue";
import StackedPlot from "../stackedPlot/StackedPlot.vue";
import configStore from "@/plugins/plot/vue/single/configuration/configStore";
import EventEmitter from "EventEmitter";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let cleanupFirst;
beforeEach((done) => {
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
cleanupFirst = [];
openmct = createOpenMct();
let telemetryPromiseResolve;
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new PlotVuePlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
spyOnBuiltins(["requestAnimationFrame"]);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
setTimeout(() => {
//Cleanup code that needs to happen before dom elements start being destroyed
cleanupFirst.forEach(cleanup => cleanup());
cleanupFirst = [];
document.body.removeChild(element);
configStore.deleteAll();
resetApplicationState(openmct).then(done);
});
});
describe("the plot views", () => {
it("provides a plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "test-object",
telemetry: {
values: [{
key: "some-key",
hints: {
domain: 1
}
},
{
key: "other-key",
hints: {
range: 1
}
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
expect(plotView).toBeDefined();
});
it("provides an overlay plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.overlay",
telemetry: {
values: [{
key: "some-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
expect(plotView).toBeDefined();
});
it("provides a stacked plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.stacked",
telemetry: {
values: [{
key: "some-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
expect(plotView).toBeDefined();
});
});
describe("The single plot view", () => {
let testTelemetryObject;
let applicableViews;
let plotViewProvider;
let plotView;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
applicableViews = openmct.objectViews.get(testTelemetryObject);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
plotView.show(child, true);
cleanupFirst.push(() => {
plotView.destroy();
});
return Vue.nextTick();
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
it("Renders an expanded legend for every telemetry", () => {
let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
legendControl.dispatchEvent(clickEvent);
let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", () => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(5);
});
it("Renders Y-axis options for the telemetry object", () => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
expect(yAxisElement.length).toBe(1);
//Object{name: "Some attribute", key: "some-key"}, Object{name: "Another attribute", key: "some-other-key"}
let options = yAxisElement[0].querySelectorAll("option");
expect(options.length).toBe(2);
expect(options[0].value).toBe("Some attribute");
expect(options[1].value).toBe("Another attribute");
});
});
describe("The stacked plot view", () => {
let testTelemetryObject;
let testTelemetryObject2;
let config;
let stackedPlotObject;
let component;
let mockComposition;
let plotViewComponentObject;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
stackedPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.stacked",
name: "Test Stacked Plot"
};
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key2",
name: "Some attribute2",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
StackedPlot
},
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject)
},
template: "<stacked-plot></stacked-plot>"
});
cleanupFirst.push(() => {
component.$destroy();
component = undefined;
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
it("Renders an expanded legend for every telemetry", () => {
let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
legendControl.dispatchEvent(clickEvent);
let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", () => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(5);
});
it("Renders Y-axis ticks for the telemetry object", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
expect(yAxisElement.length).toBe(1);
let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(6);
done();
});
});
it("Renders Y-axis options for the telemetry object", () => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
expect(yAxisElement.length).toBe(1);
let options = yAxisElement[0].querySelectorAll("option");
expect(options.length).toBe(2);
expect(options[0].value).toBe("Some attribute");
expect(options[1].value).toBe("Another attribute");
});
it("turns on cursor Guides all telemetry objects", (done) => {
expect(plotViewComponentObject.cursorGuide).toBeFalse();
plotViewComponentObject.toggleCursorGuide();
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].cursorGuide).toBeTrue();
done();
});
});
it("shows grid lines for all telemetry objects", () => {
expect(plotViewComponentObject.gridLines).toBeTrue();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(2);
});
it("hides grid lines for all telemetry objects", (done) => {
expect(plotViewComponentObject.gridLines).toBeTrue();
plotViewComponentObject.toggleGridLines();
Vue.nextTick(() => {
expect(plotViewComponentObject.gridLines).toBeFalse();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(0);
done();
});
});
it('plots a new series when a new telemetry object is added', (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it('removes plots from series when a telemetry object is removed', (done) => {
mockComposition.emit('remove', testTelemetryObject.identifier);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(0);
done();
});
});
it("Changes the label of the y axis when the option changes", (done) => {
let selectEl = element.querySelector('.gl-plot-y-label__select');
selectEl.value = 'Another attribute';
selectEl.dispatchEvent(new Event("change"));
Vue.nextTick(() => {
expect(config.yAxis.get('label')).toEqual('Another attribute');
done();
});
});
it("Renders a new series when added to one of the plots", (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it("Adds a new point to the plot", (done) => {
let originalLength = config.series.models[0].data.length;
config.series.models[0].add({
utc: 2,
'some-key': 1,
'some-other-key': 2
});
Vue.nextTick(() => {
expect(config.series.models[0].data.length).toEqual(originalLength + 1);
done();
});
});
it("updates the xscale", (done) => {
config.xAxis.set('displayRange', {
min: 0,
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].xScale.domain()).toEqual({
min: 0,
max: 10
});
done();
});
});
it("updates the yscale", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].yScale.domain()).toEqual({
min: 10,
max: 20
});
done();
});
});
});
});

View File

@ -0,0 +1,89 @@
const e10 = Math.sqrt(50);
const e5 = Math.sqrt(10);
const e2 = Math.sqrt(2);
/**
* Nicely formatted tick steps from d3-array.
*/
function tickStep(start, stop, count) {
const step0 = Math.abs(stop - start) / Math.max(0, count);
let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10));
const error = step0 / step1;
if (error >= e10) {
step1 *= 10;
} else if (error >= e5) {
step1 *= 5;
} else if (error >= e2) {
step1 *= 2;
}
return stop < start ? -step1 : step1;
}
/**
* Find the precision (number of decimals) of a step. Used to round
* ticks to precise values.
*/
function getPrecision(step) {
const exponential = step.toExponential();
const i = exponential.indexOf('e');
if (i === -1) {
return 0;
}
let precision = Math.max(0, -(Number(exponential.slice(i + 1))));
if (precision > 20) {
precision = 20;
}
return precision;
}
/**
* Linear tick generation from d3-array.
*/
export function ticks(start, stop, count) {
const step = tickStep(start, stop, count);
const precision = getPrecision(step);
return _.range(
Math.ceil(start / step) * step,
Math.floor(stop / step) * step + step / 2, // inclusive
step
).map(function round(tick) {
return Number(tick.toFixed(precision));
});
}
export function commonPrefix(a, b) {
const maxLen = Math.min(a.length, b.length);
let breakpoint = 0;
for (let i = 0; i < maxLen; i++) {
if (a[i] !== b[i]) {
break;
}
if (a[i] === ' ') {
breakpoint = i + 1;
}
}
return a.slice(0, breakpoint);
}
export function commonSuffix(a, b) {
const maxLen = Math.min(a.length, b.length);
let breakpoint = 0;
for (let i = 0; i <= maxLen; i++) {
if (a[a.length - i] !== b[b.length - i]) {
break;
}
if ('. '.indexOf(a[a.length - i]) !== -1) {
breakpoint = i;
}
}
return a.slice(a.length - breakpoint);
}

View File

@ -0,0 +1,195 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div v-show="!hideExportButtons"
class="c-control-bar"
>
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportPNG()"
>
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
title="Export This View's Data as JPG"
@click="exportJPG()"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
<stacked-plot-item v-for="object in compositionObjects"
:key="object.id"
class="c-plot--stacked-container"
:object="object"
:grid-lines="gridLines"
:cursor-guide="cursorGuide"
:plot-tick-width="maxTickWidth"
@plotTickWidth="onTickWidthChange"
@loadingUpdated="loadingUpdated"
/>
</div>
</template>
<script>
import eventHelpers from "@/plugins/plot/vue/single/lib/eventHelpers";
import StackedPlotItem from "@/plugins/plot/vue/stackedPlot/StackedPlotItem.vue";
export default {
components: {
StackedPlotItem
},
inject: ['openmct', 'domainObject', 'composition'],
data() {
return {
hideExportButtons: false,
cursorGuide: false,
gridLines: true,
loading: false,
compositionObjects: []
};
},
computed: {
maxTickWidth() {
return Math.max(...Object.values(this.tickWidthMap));
}
},
beforeDestroy() {
this.destroy();
},
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.tickWidthMap = {};
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.on('reorder', this.compositionReorder);
this.composition.load();
},
methods: {
loadingUpdated(loaded) {
this.loading = loaded;
},
destroy() {
this.stopListening();
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder);
},
addChild(child) {
const id = this.openmct.objects.makeKeyString(child.identifier);
this.tickWidthMap[id] = 0;
this.compositionObjects.push(child);
},
removeChild(childIdentifier) {
const id = this.openmct.objects.makeKeyString(childIdentifier);
delete this.tickWidthMap[id];
const childObj = this.compositionObjects.filter((c) => {
const identifier = this.openmct.objects.makeKeyString(c.identifier);
return identifier === id;
})[0];
if (childObj) {
const index = this.compositionObjects.indexOf(childObj);
this.compositionObjects.splice(index, 1);
}
},
compositionReorder(reorderPlan) {
let oldComposition = this.compositionObjects.slice();
reorderPlan.forEach((reorder) => {
this.compositionObjects[reorder.newIndex] = oldComposition[reorder.oldIndex];
});
},
resetTelemetryAndTicks(domainObject) {
this.compositionObjects = [];
this.tickWidthMap = {};
},
exportJPG() {
this.hideExportButtons = true;
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));
},
exportPNG() {
this.hideExportButtons = true;
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
},
toggleGridLines() {
this.gridLines = !this.gridLines;
},
onTickWidthChange(width, plotId) {
if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) {
return;
}
//update the tickWidth for this plotId, the computed max tick width of the stacked plot will be cascaded down
//TODO: Might need to do this using $set
this.tickWidthMap[plotId] = Math.max(width, this.tickWidthMap[plotId]);
// const newTickWidth = Math.max(...Object.values(this.tickWidthMap));
// if (newTickWidth !== tickWidth || width !== tickWidth) {
// tickWidth = newTickWidth;
// $scope.$broadcast('plot:tickWidth', tickWidth);
// }
}
}
};
</script>

View File

@ -0,0 +1,130 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div></div>
</template>
<script>
import MctPlot from '../single/MctPlot.vue';
import Vue from "vue";
export default {
inject: ['openmct', 'domainObject'],
props: {
object: {
type: Object,
default() {
return {};
}
},
gridLines: {
type: Boolean,
default() {
return true;
}
},
cursorGuide: {
type: Boolean,
default() {
return true;
}
},
plotTickWidth: {
type: Number,
default() {
return 0;
}
}
},
watch: {
gridLines(newGridLines) {
this.updateComponentProp('gridLines', newGridLines);
},
cursorGuide(newCursorGuide) {
this.updateComponentProp('cursorGuide', newCursorGuide);
},
plotTickWidth(width) {
this.updateComponentProp('plotTickWidth', width);
}
},
mounted() {
this.updateView();
},
methods: {
updateComponentProp(prop, value) {
if (this.component) {
this.component[prop] = value;
}
},
updateView() {
if (this.component) {
this.component.$destroy();
this.component = undefined;
this.$el.innerHTML = '';
}
const onTickWidthChange = this.onTickWidthChange;
const loadingUpdated = this.loadingUpdated;
const openmct = this.openmct;
const object = this.object;
const getProps = this.getProps;
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
this.component = new Vue({
el: viewContainer,
components: {
MctPlot
},
provide: {
openmct,
domainObject: object
},
data() {
return {
...getProps(),
onTickWidthChange,
loadingUpdated
};
},
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" @plotTickWidth="onTickWidthChange" @loadingUpdated="loadingUpdated"/></div>'
});
},
onTickWidthChange() {
this.$emit('plotTickWidth', ...arguments);
},
loadingUpdated(loaded) {
this.loading = loaded;
},
getProps() {
return {
gridLines: this.gridLines,
cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth,
loading: this.loading
};
}
}
};
</script>

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import StackedPlot from './StackedPlot.vue';
import Vue from 'vue';
export default function StackedPlotViewProvider(openmct) {
return {
key: 'plot-stacked',
name: 'Stacked Plot',
cssClass: 'icon-telemetry',
canView(domainObject) {
return domainObject.type === 'telemetry.plot.stacked';
},
canEdit(domainObject) {
return domainObject.type === 'telemetry.plot.stacked';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
StackedPlot
},
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject)
},
template: '<stacked-plot></stacked-plot>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -35,6 +35,7 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
'./plot/plugin',
'./plot/vue/single/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin',
'./notebook/plugin',
@ -78,6 +79,7 @@ define([
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
PlotVuePlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
@ -172,6 +174,7 @@ define([
plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin;
plugins.PlotVue = PlotVuePlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;

View File

@ -1,38 +0,0 @@
{
"ROVER": [
{
"name": "Activity 1",
"start": 1597170002854,
"end": 1597171032854,
"type": "ROVER",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Activity 2",
"start": 1597171132854,
"end": 1597171232854,
"type": "ROVER",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Activity 4",
"start": 1597171132854,
"end": 1597171232854,
"type": "ROVER",
"color": "fuchsia",
"textColor": "black"
}
],
"VIPER": [
{
"name": "Activity 3",
"start": 1597170132854,
"end": 1597171202854,
"type": "VIPER",
"color": "fuchsia",
"textColor": "black"
}
]
}

View File

@ -76,7 +76,7 @@ mct-plot {
margin-bottom: $interiorMargin;
}
.l-view-section {
.l-view-section, .c-plot--stacked-container {
display: flex;
flex: 1 1 auto;
flex-direction: column;
@ -143,6 +143,14 @@ mct-plot {
left: 0;
}
.gl-plot-chart-area, .gl-plot-chart-wrapper {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.gl-plot-axis-area.gl-plot-x {
top: auto;
right: 0;
@ -196,6 +204,10 @@ mct-plot {
}
}
.gl-plot-ticks, .gl-plot-tick-wrapper {
height: 100%;
}
.gl-plot-label,
.l-plot-label {
position: absolute;

View File

@ -65,7 +65,7 @@
v-for="(ancestor, index) in focusedAncestors"
:key="ancestor.id"
:node="ancestor"
:show-up="index !== 0 && index < focusedAncestors.length && initialLoad"
:show-up="index !== 0 && index < focusedAncestors.length && initialLoad"
:show-down="false"
:left-offset="index * 10 + 'px'"
@resetTree="beginNavigationRequest('handleReset', ancestor)"

View File

@ -82,6 +82,7 @@ class ApplicationRouter extends EventEmitter {
destroy() {
this.locationBar.stop();
this.removeAllListeners();
}
handleLocationChange(pathString) {