Multiple Y-Axes for Overlay Plots (#6153)

Support multiple y-axes in overlay plots

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Rukmini Bose <rukmini.bose15@gmail.com>
This commit is contained in:
Jesse Mazzella 2023-01-23 07:34:26 -08:00 committed by GitHub
parent 9980aab18f
commit 1b71a3bf33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2234 additions and 372 deletions

View File

@ -156,7 +156,7 @@ async function turnOffAutoscale(page) {
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
// save
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();

View File

@ -205,7 +205,8 @@ async function enableEditMode(page) {
*/
async function enableLogMode(page) {
// turn on log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
}
/**
@ -213,7 +214,7 @@ async function enableLogMode(page) {
*/
async function disableLogMode(page) {
// turn off log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
}
/**

View File

@ -0,0 +1,124 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Overlay Plot', () => {
test('Plot legend color is in sync with plot series color', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
// gets color for swatch located in legend
const element = await page.waitForSelector('.plot-series-color-swatch');
const color = await element.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(color).toBe('rgb(255, 166, 61)');
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
name: 'swg a',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
name: 'swg b',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
name: 'swg c',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
name: 'swg d',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
name: 'swg e',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
// Expand the elements pool vertically
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
// Drag swg a, c, e into Y Axis 2
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
// Drag swg b into Y Axis 3
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
const yAxis1Group = page.getByLabel("Y Axis 1");
const yAxis2Group = page.getByLabel("Y Axis 2");
const yAxis3Group = page.getByLabel("Y Axis 3");
// Verify that the elements are in the correct buckets and in the correct order
expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy();
expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy();
expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy();
});
});

View File

@ -34,23 +34,27 @@
@legendHoverChanged="legendHoverChanged"
/>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<y-axis
v-if="seriesModels.length > 0"
:tick-width="tickWidth"
:single-series="seriesModels.length === 1"
:has-same-range-value="hasSameRangeValue"
:series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
}"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
/>
<div
v-if="seriesModels.length"
class="u-contents"
>
<y-axis
v-for="(yAxis, index) in yAxesIds"
:id="yAxis.id"
:key="`yAxis-${yAxis.id}-${index}`"
:multiple-left-axes="multipleLeftAxes"
:position="yAxis.id > 2 ? 'right' : 'left'"
:class="{'plot-yaxis-right': yAxis.id > 2}"
:tick-width="yAxis.tickWidth"
:plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
@toggleAxisVisibility="toggleSeriesForYAxis"
/>
</div>
<div
class="gl-plot-wrapper-display-area-and-x-axis"
:style="{
left: (plotWidth + 20) + 'px'
}"
:style="xAxisStyle"
>
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
@ -69,9 +73,12 @@
/>
<mct-ticks
v-for="(yAxis, index) in yAxesIds"
v-show="gridLines"
:key="`yAxis-gridlines-${index}`"
:axis-type="'yAxis'"
:position="'bottom'"
:axis-id="yAxis.id"
@plotTickWidth="onTickWidthChange"
/>
@ -88,6 +95,7 @@
:annotated-points="annotatedPoints"
:annotation-selections="annotationSelections"
:show-limit-line-labels="showLimitLineLabels"
:hidden-y-axis-ids="hiddenYAxisIds"
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
@plotReinitializeCanvas="initCanvas"
@chartLoaded="initialize"
@ -218,6 +226,7 @@ import KDBush from 'kdbush';
import _ from "lodash";
const OFFSET_THRESHOLD = 10;
const AXES_PADDING = 20;
export default {
components: {
@ -275,7 +284,6 @@ export default {
annotatedPoints: [],
annotationSelections: [],
lockHighlightPoint: false,
tickWidth: 0,
yKeyOptions: [],
yAxisLabel: '',
rectangles: [],
@ -290,12 +298,33 @@ export default {
isTimeOutOfSync: false,
showLimitLineLabels: this.limitLineLabels,
isFrozenOnMouseDown: false,
hasSameRangeValue: true,
cursorGuide: this.initCursorGuide,
gridLines: this.initGridLines
gridLines: this.initGridLines,
yAxes: [],
hiddenYAxisIds: [],
yAxisListWithRange: []
};
},
computed: {
xAxisStyle() {
const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2);
const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING;
let style = {
left: `${this.plotLeftTickWidth + leftOffset}px`
};
if (rightAxis) {
style.right = `${rightAxis.tickWidth + AXES_PADDING}px`;
}
return style;
},
yAxesIds() {
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
},
multipleLeftAxes() {
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1;
},
isNestedWithinAStackedPlot() {
const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
@ -322,8 +351,17 @@ export default {
return 'plot-legend-collapsed';
}
},
plotWidth() {
return this.plotTickWidth || this.tickWidth;
plotLeftTickWidth() {
let leftTickWidth = 0;
this.yAxes.forEach((yAxis) => {
if (yAxis.id > 2) {
return;
}
leftTickWidth = leftTickWidth + yAxis.tickWidth;
});
return this.plotTickWidth || leftTickWidth;
}
},
watch: {
@ -341,6 +379,7 @@ export default {
}
},
mounted() {
this.yAxisIdVisibility = {};
this.offsetWidth = 0;
document.addEventListener('keydown', this.handleKeyDown);
@ -352,6 +391,20 @@ export default {
this.config = this.getConfig();
this.legend = this.config.legend;
this.yAxes = [{
id: this.config.yAxis.id,
seriesCount: 0,
tickWidth: 0
}];
if (this.config.additionalYAxes) {
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
return {
id: yAxis.id,
seriesCount: 0,
tickWidth: 0
};
}));
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('configLoaded', configId);
@ -373,6 +426,8 @@ export default {
this.openmct.selection.on('change', this.updateSelection);
this.setTimeContext();
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
this.loaded = true;
},
beforeDestroy() {
@ -456,8 +511,10 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
@ -490,33 +547,41 @@ export default {
return config;
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, 1);
this.$set(this.seriesModels, index, series);
this.listenTo(series, 'change:xKey', (xKey) => {
this.setDisplayRange(series, xKey);
}, this);
this.listenTo(series, 'change:yKey', () => {
this.checkSameRangeValue();
this.loadSeriesData(series);
}, this);
this.listenTo(series, 'change:interpolate', () => {
this.loadSeriesData(series);
}, this);
this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);
this.checkSameRangeValue();
this.loadSeriesData(series);
},
checkSameRangeValue() {
this.hasSameRangeValue = this.seriesModels.every((model) => {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
removeSeries(plotSeries, index) {
const yAxisId = plotSeries.get('yAxisId');
this.updateAxisUsageCount(yAxisId, -1);
this.seriesModels.splice(index, 1);
this.stopListening(plotSeries);
},
removeSeries(plotSeries, index) {
this.seriesModels.splice(index, 1);
this.checkSameRangeValue();
this.stopListening(plotSeries);
updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {
this.updateAxisUsageCount(oldAxisId, -1);
this.updateAxisUsageCount(newAxisId, 1);
},
updateAxisUsageCount(yAxisId, updateCountBy) {
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
if (foundYAxis) {
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
}
},
async loadAnnotations() {
if (!this.openmct.annotation.getAvailableTags().length) {
@ -832,7 +897,13 @@ export default {
// Setup canvas etc.
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
this.yScale = [];
this.yAxisListWithRange.forEach((yAxis) => {
this.yScale.push({
id: yAxis.id,
scale: new LinearScale(yAxis.get('displayRange'))
});
});
this.pan = undefined;
this.marquee = undefined;
@ -848,7 +919,9 @@ export default {
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);
this.yAxisListWithRange.forEach((yAxis) => {
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
});
},
onXAxisChange(displayBounds) {
@ -857,26 +930,45 @@ export default {
}
},
onYAxisChange(displayBounds) {
onYAxisChange(yAxisId, displayBounds) {
if (displayBounds) {
this.yScale.domain(displayBounds);
this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => {
yAxis.scale.domain(displayBounds);
});
}
},
onTickWidthChange(width, fromDifferentObject) {
if (fromDifferentObject) {
onTickWidthChange(data, fromDifferentObject) {
const {width, yAxisId} = data;
if (yAxisId) {
const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId);
if (fromDifferentObject) {
// Always accept tick width if it comes from a different object.
this.tickWidth = width;
} else {
this.yAxes[index].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;
const newWidth = Math.max(width, this.yAxes[index].tickWidth);
if (newWidth !== this.yAxes[index].tickWidth) {
this.yAxes[index].tickWidth = newWidth;
}
}
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
}
},
toggleSeriesForYAxis({ id, visible}) {
//if toggling to visible, re-fetch the data for the series that are part of this y Axis
if (visible === true) {
this.config.series.models.filter(model => model.get('yAxisId') === id)
.forEach(this.loadSeriesData, this);
}
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.tickWidth, id);
this.yAxisIdVisibility[id] = visible;
this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => {
return this.yAxisIdVisibility[key] === false;
});
},
trackMousePosition(event) {
@ -885,9 +977,11 @@ export default {
min: 0,
max: this.chartElementBounds.width
});
this.yScale.range({
min: 0,
max: this.chartElementBounds.height
this.yScale.forEach((yAxis) => {
yAxis.scale.range({
min: 0,
max: this.chartElementBounds.height
});
});
this.positionOverElement = {
@ -896,9 +990,13 @@ export default {
- (event.clientY - this.chartElementBounds.top)
};
const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y));
const yAxisIds = this.yScale.map((yAxis) => yAxis.id);
// Also store the order of yAxisIds so that we can associate the y location to the yAxis
this.positionOverPlot = {
x: this.xScale.invert(this.positionOverElement.x),
y: this.yScale.invert(this.positionOverElement.y)
y: yLocationForPositionOverPlot,
yAxisIds
};
if (this.cursorGuide) {
@ -911,6 +1009,12 @@ export default {
event.preventDefault();
},
getYPositionForYAxis(object, yAxis) {
const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id'));
return object.y[index];
},
updateCrosshairs(event) {
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
@ -1017,8 +1121,9 @@ export default {
}
const { start, end } = this.marquee;
const someYPositionOverPlot = start.y.some(y => y);
return start.x === end.x && start.y === end.y;
return start.x === end.x && someYPositionOverPlot;
},
updateMarquee() {
@ -1179,9 +1284,15 @@ export default {
},
endAnnotationMarquee(event) {
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
const minY = Math.min(this.marquee.start.y, this.marquee.end.y);
const startMinY = this.marquee.start.y.reduce((previousY, currentY) => {
return Math.min(previousY, currentY);
}, this.marquee.start.y[0]);
const endMinY = this.marquee.end.y.reduce((previousY, currentY) => {
return Math.min(previousY, currentY);
}, this.marquee.end.y[0]);
const minY = Math.min(startMinY, endMinY);
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
const maxY = Math.max(this.marquee.start.y, this.marquee.end.y);
const maxY = Math.max(startMinY, endMinY);
const boundingBox = {
minX,
minY,
@ -1205,9 +1316,13 @@ export default {
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.yAxisListWithRange.forEach((yAxis) => {
const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis);
const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis);
yAxis.set('displayRange', {
min: Math.min(yStartPosition, yEndPosition),
max: Math.max(yStartPosition, yEndPosition)
});
});
this.userViewportChangeEnd();
} else {
@ -1238,11 +1353,17 @@ export default {
zoom(zoomDirection, zoomFactor) {
const currentXaxis = this.config.xAxis.get('displayRange');
const currentYaxis = this.config.yAxis.get('displayRange');
let doesYAxisHaveRange = false;
this.yAxisListWithRange.forEach((yAxisModel) => {
if (yAxisModel.get('displayRange')) {
doesYAxisHaveRange = true;
}
});
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!currentXaxis || !currentYaxis) {
if (!currentXaxis || !doesYAxisHaveRange) {
return;
}
@ -1250,7 +1371,6 @@ export default {
this.trackHistory();
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
if (zoomDirection === 'in') {
this.config.xAxis.set('displayRange', {
@ -1258,9 +1378,17 @@ export default {
max: currentXaxis.max - xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
this.yAxisListWithRange.forEach((yAxisModel) => {
const currentYaxis = yAxisModel.get('displayRange');
if (!currentYaxis) {
return;
}
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
yAxisModel.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
});
});
} else if (zoomDirection === 'out') {
this.config.xAxis.set('displayRange', {
@ -1268,9 +1396,17 @@ export default {
max: currentXaxis.max + xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
this.yAxisListWithRange.forEach((yAxisModel) => {
const currentYaxis = yAxisModel.get('displayRange');
if (!currentYaxis) {
return;
}
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
yAxisModel.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
});
});
}
@ -1287,11 +1423,17 @@ export default {
}
let xDisplayRange = this.config.xAxis.get('displayRange');
let yDisplayRange = this.config.yAxis.get('displayRange');
let doesYAxisHaveRange = false;
this.yAxisListWithRange.forEach((yAxisModel) => {
if (yAxisModel.get('displayRange')) {
doesYAxisHaveRange = true;
}
});
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!xDisplayRange || !yDisplayRange) {
if (!xDisplayRange || !doesYAxisHaveRange) {
return;
}
@ -1299,22 +1441,19 @@ export default {
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) {
const yRangeList = [];
this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange')));
plotHistoryStep = {
x: xDisplayRange,
y: yDisplayRange
x: this.config.xAxis.get('displayRange'),
y: yRangeList
};
}
@ -1325,20 +1464,47 @@ export default {
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.yAxisListWithRange.forEach((yAxisModel) => {
const yDisplayRange = yAxisModel.get('displayRange');
if (!yDisplayRange) {
return;
}
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let yDistMouseToMax = yDisplayRange.max - yPosition;
let yDistMouseToMin = yPosition - yDisplayRange.min;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
yAxisModel.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.yAxisListWithRange.forEach((yAxisModel) => {
const yDisplayRange = yAxisModel.get('displayRange');
if (!yDisplayRange) {
return;
}
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let yDistMouseToMax = yDisplayRange.max - yPosition;
let yDistMouseToMin = yPosition - yDisplayRange.min;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
yAxisModel.set('displayRange', {
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
});
}
@ -1371,24 +1537,48 @@ export default {
}
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
const dY = [];
this.positionOverPlot.y.forEach((yAxisPosition, index) => {
const yAxisId = this.positionOverPlot.yAxisIds[index];
dY.push({
yAxisId: yAxisId,
y: this.pan.start.y[index] - yAxisPosition
});
});
this.yAxisListWithRange.forEach((yAxis) => {
const yRange = yAxis.get('displayRange');
if (!yRange) {
return;
}
const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id'));
yAxis.set('displayRange', {
min: yRange.min + dY[yIndex].y,
max: yRange.max + dY[yIndex].y
});
});
},
trackHistory() {
const yRangeList = [];
const yAxisIds = [];
this.yAxisListWithRange.forEach((yAxis) => {
yRangeList.push(yAxis.get('displayRange'));
yAxisIds.push(yAxis.get('id'));
});
this.plotHistory.push({
x: this.config.xAxis.get('displayRange'),
y: this.config.yAxis.get('displayRange')
y: yRangeList,
yAxisIds
});
},
@ -1398,7 +1588,9 @@ export default {
},
freeze() {
this.config.yAxis.set('frozen', true);
this.yAxisListWithRange.forEach((yAxis) => {
yAxis.set('frozen', true);
});
this.config.xAxis.set('frozen', true);
this.setStatus();
},
@ -1409,7 +1601,9 @@ export default {
},
clearPanZoomHistory() {
this.config.yAxis.set('frozen', false);
this.yAxisListWithRange.forEach((yAxis) => {
yAxis.set('frozen', false);
});
this.config.xAxis.set('frozen', false);
this.setStatus();
this.plotHistory = [];
@ -1424,12 +1618,17 @@ export default {
}
this.config.xAxis.set('displayRange', previousAxisRanges.x);
this.config.yAxis.set('displayRange', previousAxisRanges.y);
this.yAxisListWithRange.forEach((yAxis) => {
const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis);
yAxis.set('displayRange', yPosition);
});
this.userViewportChangeEnd();
},
setYAxisKey(yKey) {
this.config.series.models[0].set('yKey', yKey);
setYAxisKey(yKey, yAxisId) {
const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId));
seriesForYAxis.forEach(model => model.set('yKey', yKey));
},
pause() {

View File

@ -103,6 +103,12 @@ export default {
return 6;
}
},
axisId: {
type: Number,
default() {
return null;
}
},
position: {
required: true,
type: String,
@ -145,7 +151,15 @@ export default {
throw new Error('config is missing');
}
return config[this.axisType];
if (this.axisType === 'yAxis') {
if (this.axisId && this.axisId !== config.yAxis.id) {
return config.additionalYAxes.find(axis => axis.id === this.axisId);
} else {
return config.yAxis;
}
} else {
return config[this.axisType];
}
},
/**
* Determine whether ticks should be regenerated for a given range.
@ -258,7 +272,10 @@ export default {
}, 0));
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', tickWidth);
this.$emit('plotTickWidth', {
width: tickWidth,
yAxisId: this.axisType === 'yAxis' ? this.axisId : ''
});
this.shouldCheckWidth = false;
}
}

View File

@ -22,19 +22,28 @@
<template>
<div
v-if="loaded"
class="gl-plot-axis-area gl-plot-y has-local-controls"
:style="{
width: (tickWidth + 20) + 'px'
}"
class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis"
:style="yAxisStyle"
>
<div
v-if="canShowYAxisLabel"
class="gl-plot-label gl-plot-y-label"
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
>{{ yAxisLabel }}
>
<span
v-for="(colorAsHexString, index) in seriesColors"
:key="`${colorAsHexString}-${index}`"
class="plot-series-color-swatch"
:style="{ 'background-color': colorAsHexString }"
>
</span>
<span
:class="{'icon-gear-after': (yKeyOptions.length > 1 && singleSeries)}"
>{{ canShowYAxisLabel ? yAxisLabel : `Y Axis ${id}` }}</span>
<span
v-if="showVisibilityToggle"
:class="{ 'icon-eye-open': visible, 'icon-eye-disabled': !visible}"
@click="toggleSeriesVisibility"
></span>
</div>
<select
v-if="yKeyOptions.length > 1 && singleSeries"
v-model="yAxisLabel"
@ -52,6 +61,7 @@
</select>
<mct-ticks
:axis-id="id"
:axis-type="'yAxis'"
class="gl-plot-ticks"
:position="'top'"
@ -63,6 +73,9 @@
<script>
import MctTicks from "../MctTicks.vue";
import configStore from "../configuration/ConfigStore";
import eventHelpers from "../lib/eventHelpers";
const AXIS_PADDING = 20;
export default {
components: {
@ -70,22 +83,10 @@ export default {
},
inject: ['openmct', 'domainObject'],
props: {
singleSeries: {
type: Boolean,
id: {
type: Number,
default() {
return true;
}
},
hasSameRangeValue: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
return {};
return 1;
}
},
tickWidth: {
@ -93,37 +94,141 @@ export default {
default() {
return 0;
}
},
plotLeftTickWidth: {
type: Number,
default() {
return 0;
}
},
multipleLeftAxes: {
type: Boolean,
default() {
return false;
}
},
position: {
type: String,
default() {
return 'left';
}
}
},
data() {
return {
yAxisLabel: 'none',
loaded: false
loaded: false,
yKeyOptions: [],
hasSameRangeValue: true,
singleSeries: true,
mainYAxisId: null,
hasAdditionalYAxes: false,
seriesColors: [],
visible: true
};
},
computed: {
showVisibilityToggle() {
return this.domainObject.type === 'telemetry.plot.overlay';
},
canShowYAxisLabel() {
return this.singleSeries === true || this.hasSameRangeValue === true;
},
yAxisStyle() {
let style = {
width: `${this.tickWidth + AXIS_PADDING}px`
};
const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0;
if (this.position === 'right') {
style.left = `-${this.tickWidth + AXIS_PADDING}px`;
} else {
const thisIsTheSecondLeftAxis = (this.id - 1) > 0;
if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) {
style.left = 0;
style['border-right'] = `1px solid`;
} else {
style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
}
}
return style;
}
},
mounted() {
this.yAxis = this.getYAxisFromConfig();
this.seriesModels = [];
eventHelpers.extend(this);
this.initAxisAndSeriesConfig();
this.loaded = true;
this.setUpYAxisOptions();
},
methods: {
getYAxisFromConfig() {
initAxisAndSeriesConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config.yAxis;
this.mainYAxisId = config.yAxis.id;
this.hasAdditionalYAxes = config?.additionalYAxes.length;
if (this.id && this.id !== this.mainYAxisId) {
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
} else {
this.yAxis = config.yAxis;
}
this.config = config;
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this);
this.config.series.models.forEach(this.addSeries, this);
}
},
addOrRemoveSeries(series) {
const yAxisId = series.get('yAxisId');
if (yAxisId === this.id) {
this.addSeries(series);
} else {
this.removeSeries(series);
}
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier')));
if (yAxisId === this.id && seriesIndex < 0) {
this.seriesModels.push(series);
this.processSeries();
this.setUpYAxisOptions();
}
this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);
},
removeSeries(plotSeries) {
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
if (seriesIndex > -1) {
this.seriesModels.splice(seriesIndex, 1);
this.processSeries();
this.setUpYAxisOptions();
}
},
processSeries() {
this.hasSameRangeValue = this.seriesModels.every((model) => {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
this.singleSeries = this.seriesModels.length === 1;
this.seriesColors = this.seriesModels.map(model => {
return model.get('color').asHexString();
});
},
setUpYAxisOptions() {
this.yKeyOptions = [];
if (!this.seriesModels.length) {
return;
}
if (this.seriesModel.metadata) {
this.yKeyOptions = this.seriesModel.metadata
const seriesModel = this.seriesModels[0];
if (seriesModel.metadata) {
this.yKeyOptions = seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
@ -135,22 +240,29 @@ export default {
// 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 ? yKeyModel.name : '';
this.yAxisLabel = this.yAxis.get('label');
}
},
toggleYAxisLabel() {
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
if (yAxisObject) {
this.$emit('yKeyChanged', yAxisObject.key);
this.$emit('yKeyChanged', yAxisObject.key, this.id);
this.yAxis.set('label', this.yAxisLabel);
}
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
onTickWidthChange(data) {
this.$emit('tickWidthChanged', {
width: data.width,
yAxisId: this.id
});
},
toggleSeriesVisibility() {
this.visible = !this.visible;
this.$emit('toggleAxisVisibility', {
id: this.id,
visible: this.visible
});
}
}
};

View File

@ -86,6 +86,12 @@ export default {
return {};
}
},
hiddenYAxisIds: {
type: Array,
default() {
return [];
}
},
annotationViewingAndEditingAllowed: {
type: Boolean,
required: true
@ -111,6 +117,12 @@ export default {
},
showLimitLineLabels() {
this.drawLimitLines();
},
hiddenYAxisIds() {
this.hiddenYAxisIds.forEach(id => {
this.resetYOffsetAndSeriesDataForYAxis(id);
});
this.scheduleDraw();
}
},
mounted() {
@ -121,7 +133,21 @@ export default {
this.limitLines = [];
this.pointSets = [];
this.alarmSets = [];
this.offset = {};
const yAxisId = this.config.yAxis.get('id');
this.offset = {
[yAxisId]: {}
};
this.listenTo(this.config.yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
if (this.config.additionalYAxes.length) {
this.config.additionalYAxes.forEach(yAxis => {
const id = yAxis.get('id');
this.offset[id] = {};
this.listenTo(yAxis, 'change', this.updateLimitsAndDraw);
this.listenTo(yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, id), this);
});
}
this.seriesElements = new WeakMap();
this.seriesLimits = new WeakMap();
@ -134,8 +160,7 @@ export default {
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.updateLimitsAndDraw);
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
this.config.series.forEach(this.onSeriesAdd, this);
this.$emit('chartLoaded');
@ -170,6 +195,7 @@ export default {
this.listenTo(series, 'change:markers', this.changeMarkers, this);
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
this.listenTo(series, 'change:yAxisId', this.resetAxisAndRedraw, this);
this.listenTo(series, 'change', this.scheduleDraw);
this.listenTo(series, 'add', this.onAddPoint);
this.makeChartElement(series);
@ -177,6 +203,7 @@ export default {
},
onAddPoint(point, insertIndex, series) {
const xRange = this.config.xAxis.get('displayRange');
//TODO: get the yAxis of this series
const yRange = this.config.yAxis.get('displayRange');
const xValue = series.getXVal(point);
const yValue = series.getYVal(point);
@ -247,6 +274,21 @@ export default {
this.makeLimitLines(series);
this.updateLimitsAndDraw();
},
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
if (!oldYAxisId) {
return;
}
//Remove the old chart elements for the series since their offsets are pointing to the old y axis
this.removeChartElement(series);
this.resetYOffsetAndSeriesDataForYAxis(oldYAxisId);
//Make the chart elements again for the new y-axis and offset
this.makeChartElement(series);
this.makeLimitLines(series);
this.scheduleDraw();
},
onSeriesRemove(series) {
this.stopListening(series);
this.removeChartElement(series);
@ -259,25 +301,33 @@ export default {
this.limitLines.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) {
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
delete this.offset[yAxisId].y;
delete this.offset[yAxisId].xVal;
delete this.offset[yAxisId].yVal;
delete this.offset[yAxisId].xKey;
delete this.offset[yAxisId].yKey;
this.resetResetChartElements(yAxisId);
},
resetResetChartElements(yAxisId) {
const lines = this.lines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
lines.forEach(function (line) {
line.reset();
});
this.limitLines.forEach(function (line) {
const limitLines = this.limitLines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
limitLines.forEach(function (line) {
line.reset();
});
this.pointSets.forEach(function (pointSet) {
const pointSets = this.pointSets.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
pointSets.forEach(function (pointSet) {
pointSet.reset();
});
},
setOffset(offsetPoint, index, series) {
if (this.offset.x && this.offset.y) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
if (this.offset[yAxisId].x && this.offset[yAxisId].y) {
return;
}
@ -286,19 +336,20 @@ export default {
y: series.getYVal(offsetPoint)
};
this.offset.x = function (x) {
this.offset[yAxisId].x = function (x) {
return x - offsets.x;
}.bind(this);
this.offset.y = function (y) {
this.offset[yAxisId].y = function (y) {
return y - offsets.y;
}.bind(this);
this.offset.xVal = function (point, pSeries) {
return this.offset.x(pSeries.getXVal(point));
this.offset[yAxisId].xVal = function (point, pSeries) {
return this.offset[yAxisId].x(pSeries.getXVal(point));
}.bind(this);
this.offset.yVal = function (point, pSeries) {
return this.offset.y(pSeries.getYVal(point));
this.offset[yAxisId].yVal = function (point, pSeries) {
return this.offset[yAxisId].y(pSeries.getYVal(point));
}.bind(this);
},
initializeCanvas(canvas, overlay) {
this.canvas = canvas;
this.overlay = overlay;
@ -346,11 +397,15 @@ export default {
this.clearLimitLines(series);
},
lineForSeries(series) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
let offset = this.offset[yAxisId];
if (series.get('interpolate') === 'linear') {
return new MCTChartLineLinear(
series,
this,
this.offset
offset
);
}
@ -358,33 +413,45 @@ export default {
return new MCTChartLineStepAfter(
series,
this,
this.offset
offset
);
}
},
limitLineForSeries(series) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
let offset = this.offset[yAxisId];
return new MCTChartAlarmLineSet(
series,
this,
this.offset,
offset,
this.openmct.time.bounds()
);
},
pointSetForSeries(series) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
let offset = this.offset[yAxisId];
if (series.get('markers')) {
return new MCTChartPointSet(
series,
this,
this.offset
offset
);
}
},
alarmPointSetForSeries(series) {
const mainYAxisId = this.config.yAxis.get('id');
const yAxisId = series.get('yAxisId') || mainYAxisId;
let offset = this.offset[yAxisId];
if (series.get('alarmMarkers')) {
return new MCTChartAlarmPointSet(
series,
this,
this.offset
offset
);
}
},
@ -445,8 +512,8 @@ export default {
this.seriesLimits.delete(series);
}
},
canDraw() {
if (!this.offset.x || !this.offset.y) {
canDraw(yAxisId) {
if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {
return false;
}
@ -469,22 +536,37 @@ export default {
}
this.drawAPI.clear();
if (this.canDraw()) {
this.updateViewport();
this.drawSeries();
this.drawRectangles();
this.drawHighlights();
const mainYAxisId = this.config.yAxis.get('id');
//There has to be at least one yAxis
const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id')));
// Repeat drawing for all yAxes
yAxisIds.forEach((id) => {
if (this.canDraw(id)) {
this.updateViewport(id);
this.drawSeries(id);
this.drawRectangles(id);
this.drawHighlights(id);
// only draw these in fixed time mode or plot is paused
if (this.annotationViewingAndEditingAllowed) {
this.drawAnnotatedPoints();
this.drawAnnotationSelections();
// only draw these in fixed time mode or plot is paused
if (this.annotationViewingAndEditingAllowed) {
this.drawAnnotatedPoints(id);
this.drawAnnotationSelections(id);
}
}
});
},
updateViewport(yAxisId) {
const mainYAxisId = this.config.yAxis.get('id');
const xRange = this.config.xAxis.get('displayRange');
let yRange;
if (yAxisId === mainYAxisId) {
yRange = this.config.yAxis.get('displayRange');
} else {
if (this.config.additionalYAxes.length) {
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
yRange = yAxisForId.get('displayRange');
}
}
},
updateViewport() {
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
if (!xRange || !yRange) {
return;
@ -495,9 +577,10 @@ export default {
yRange.max - yRange.min
];
const origin = [
this.offset.x(xRange.min),
this.offset.y(yRange.min)
let origin;
origin = [
this.offset[yAxisId].x(xRange.min),
this.offset[yAxisId].y(yRange.min)
];
this.drawAPI.setDimensions(
@ -505,38 +588,71 @@ export default {
origin
);
},
drawSeries() {
this.lines.forEach(this.drawLine, this);
this.pointSets.forEach(this.drawPoints, this);
this.alarmSets.forEach(this.drawAlarmPoints, this);
// match items by their yAxisId, but don't care if the series is hidden or not.
matchByYAxisIdExcludingVisibility() {
const args = Array.from(arguments).slice(0, 4);
return this.matchByYAxisId(...args, true);
},
matchByYAxisId(id, item, index, items, excludeVisibility = false) {
const mainYAxisId = this.config.yAxis.get('id');
let matchesId = false;
const axisSeriesAreVisible = excludeVisibility || this.hiddenYAxisIds.indexOf(id) < 0;
const series = item.series;
if (axisSeriesAreVisible && series) {
const seriesYAxisId = series.get('yAxisId') || mainYAxisId;
matchesId = seriesYAxisId === id;
}
return matchesId;
},
drawSeries(id) {
const lines = this.lines.filter(this.matchByYAxisId.bind(this, id));
lines.forEach(this.drawLine, this);
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id));
pointSets.forEach(this.drawPoints, this);
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
alarmSets.forEach(this.drawAlarmPoints, this);
},
drawLimitLines() {
if (this.canDraw()) {
this.updateViewport();
if (!this.drawAPI.origin) {
return;
}
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
let limitPointOverlap = [];
this.limitLines.forEach((limitLine) => {
let limitContainerEl = this.$refs.limitArea;
limitLine.limits.forEach((limit) => {
const showLabels = this.showLabels(limit.seriesKey);
if (showLabels) {
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
limitPointOverlap.push(overlap);
let limitLabelEl = this.getLimitLabel(limit, overlap);
limitContainerEl.appendChild(limitLabelEl);
}
let limitEl = this.getLimitElement(limit);
limitContainerEl.appendChild(limitEl);
}, this);
});
this.config.series.models.forEach(series => {
const yAxisId = series.get('yAxisId');
this.drawLimitLinesForSeries(yAxisId, series);
});
},
drawLimitLinesForSeries(yAxisId, series) {
if (!this.canDraw(yAxisId)) {
return;
}
this.updateViewport(yAxisId);
if (!this.drawAPI.origin) {
return;
}
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
let limitPointOverlap = [];
this.limitLines.forEach((limitLine) => {
let limitContainerEl = this.$refs.limitArea;
limitLine.limits.forEach((limit) => {
if (!series.includes(limit.seriesKey)) {
return;
}
const showLabels = this.showLabels(limit.seriesKey);
if (showLabels) {
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
limitPointOverlap.push(overlap);
let limitLabelEl = this.getLimitLabel(limit, overlap);
limitContainerEl.appendChild(limitLabelEl);
}
let limitEl = this.getLimitElement(limit);
limitContainerEl.appendChild(limitEl);
}, this);
});
},
showLabels(seriesKey) {
return this.showLimitLineLabels.seriesKey
@ -618,12 +734,14 @@ export default {
);
},
drawLine(chartElement, disconnected) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
disconnected
);
if (chartElement) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
disconnected
);
}
},
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
@ -632,18 +750,26 @@ export default {
return ((xValue > xRange.min) && (xValue < xRange.max)
&& (yValue > yRange.min) && (yValue < yRange.max));
},
drawAnnotatedPoints() {
drawAnnotatedPoints(yAxisId) {
// we should do this by series, and then plot all the points at once instead
// of doing it one by one
if (this.annotatedPoints && this.annotatedPoints.length) {
const uniquePointsToDraw = [];
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
this.annotatedPoints.forEach((annotatedPoint) => {
let yRange;
if (yAxisId === this.config.yAxis.get('id')) {
yRange = this.config.yAxis.get('displayRange');
} else if (this.config.additionalYAxes.length) {
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
yRange = yAxisForId.get('displayRange');
}
const annotatedPoints = this.annotatedPoints.filter(this.matchByYAxisId.bind(this, yAxisId));
annotatedPoints.forEach((annotatedPoint) => {
// if the annotation is outside the range, don't draw it
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
const canvasXValue = this.offset.xVal(annotatedPoint.point, annotatedPoint.series);
const canvasYValue = this.offset.yVal(annotatedPoint.point, annotatedPoint.series);
const canvasXValue = this.offset[yAxisId].xVal(annotatedPoint.point, annotatedPoint.series);
const canvasYValue = this.offset[yAxisId].yVal(annotatedPoint.point, annotatedPoint.series);
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
@ -667,15 +793,16 @@ export default {
this.drawAPI.drawPoints(pointToDraw, color, pointCount, ANNOTATION_SIZE, shape);
}
},
drawAnnotationSelections() {
drawAnnotationSelections(yAxisId) {
if (this.annotationSelections && this.annotationSelections.length) {
this.annotationSelections.forEach(this.drawAnnotationSelection, this);
const annotationSelections = this.annotationSelections.filter(this.matchByYAxisId.bind(this, yAxisId));
annotationSelections.forEach(this.drawAnnotationSelection.bind(this, yAxisId), this);
}
},
drawAnnotationSelection(annotationSelection) {
drawAnnotationSelection(yAxisId, annotationSelection) {
const points = new Float32Array([
this.offset.xVal(annotationSelection.point, annotationSelection.series),
this.offset.yVal(annotationSelection.point, annotationSelection.series)
this.offset[yAxisId].xVal(annotationSelection.point, annotationSelection.series),
this.offset[yAxisId].yVal(annotationSelection.point, annotationSelection.series)
]);
const color = [255, 255, 255, 1]; // white
@ -684,15 +811,16 @@ export default {
this.drawAPI.drawPoints(points, color, pointCount, ANNOTATION_SIZE, shape);
},
drawHighlights() {
drawHighlights(yAxisId) {
if (this.highlights && this.highlights.length) {
this.highlights.forEach(this.drawHighlight, this);
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
}
},
drawHighlight(highlight) {
drawHighlight(yAxisId, highlight) {
const points = new Float32Array([
this.offset.xVal(highlight.point, highlight.series),
this.offset.yVal(highlight.point, highlight.series)
this.offset[yAxisId].xVal(highlight.point, highlight.series),
this.offset[yAxisId].yVal(highlight.point, highlight.series)
]);
const color = highlight.series.get('color').asRGBAArray();
@ -701,23 +829,31 @@ export default {
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
},
drawRectangles() {
drawRectangles(yAxisId) {
if (this.rectangles) {
this.rectangles.forEach(this.drawRectangle, this);
this.rectangles.forEach(this.drawRectangle.bind(this, yAxisId), 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
);
drawRectangle(yAxisId, rect) {
if (!rect.start.yAxisIds || !rect.end.yAxisIds) {
return;
}
const startYIndex = rect.start.yAxisIds.findIndex(id => id === yAxisId);
const endYIndex = rect.end.yAxisIds.findIndex(id => id === yAxisId);
if (rect.start.y[startYIndex] && rect.end.y[endYIndex]) {
this.drawAPI.drawSquare(
[
this.offset[yAxisId].x(rect.start.x),
this.offset[yAxisId].y(rect.start.y[startYIndex])
],
[
this.offset[yAxisId].x(rect.end.x),
this.offset[yAxisId].y(rect.end.y[endYIndex])
],
rect.color
);
}
}
}
};

View File

@ -27,6 +27,10 @@ import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
const MAX_Y_AXES = 3;
const MAIN_Y_AXES_ID = 1;
const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The individual parts of the plot configuration model
@ -58,8 +62,35 @@ export default class PlotConfigurationModel extends Model {
this.yAxis = new YAxisModel({
model: options.model.yAxis,
plot: this,
openmct: options.openmct
openmct: options.openmct,
id: options.model.yAxis.id || MAIN_Y_AXES_ID
});
//Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis
//Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES
this.additionalYAxes = [];
if (Array.isArray(options.model.additionalYAxes)) {
const maxLength = Math.min(MAX_ADDITIONAL_AXES, options.model.additionalYAxes.length);
for (let yAxisCount = 0; yAxisCount < maxLength; yAxisCount++) {
const yAxis = options.model.additionalYAxes[yAxisCount];
this.additionalYAxes.push(new YAxisModel({
model: yAxis,
plot: this,
openmct: options.openmct,
id: yAxis.id || (MAIN_Y_AXES_ID + yAxisCount + 1)
}));
}
}
// If the saved options config doesn't include information about all the additional axes, we initialize the remaining here
for (let axesCount = this.additionalYAxes.length; axesCount < MAX_ADDITIONAL_AXES; axesCount++) {
this.additionalYAxes.push(new YAxisModel({
plot: this,
openmct: options.openmct,
id: MAIN_Y_AXES_ID + axesCount + 1
}));
}
// end add additional axes
this.legend = new LegendModel({
model: options.model.legend,
plot: this,
@ -81,6 +112,9 @@ export default class PlotConfigurationModel extends Model {
}
this.yAxis.listenToSeriesCollection(this.series);
this.additionalYAxes.forEach(yAxis => {
yAxis.listenToSeriesCollection(this.series);
});
this.legend.listenToSeriesCollection(this.series);
this.listenTo(this, 'destroy', this.onDestroy, this);
@ -145,6 +179,7 @@ export default class PlotConfigurationModel extends Model {
domainObject: options.domainObject,
xAxis: {},
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []),
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
};
}

View File

@ -83,10 +83,6 @@ export default class PlotSeries extends Model {
// Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
this.xRangeMin = Number.MIN_SAFE_INTEGER;
this.yRangeMin = Number.MIN_SAFE_INTEGER;
this.xRangeMax = Number.MAX_SAFE_INTEGER;
this.yRangeMax = Number.MAX_SAFE_INTEGER;
this.unPlottableValues = [undefined, Infinity, -Infinity];
}
@ -122,7 +118,8 @@ export default class PlotSeries extends Model {
markerShape: 'point',
markerSize: 2.0,
alarmMarkers: true,
limitLines: false
limitLines: false,
yAxisId: options.model.yAxisId || 1
};
}

View File

@ -56,6 +56,9 @@ export default class SeriesCollection extends Collection {
const series = this.byIdentifier(seriesConfig.identifier);
if (series) {
series.persistedConfig = seriesConfig;
if (series.get('yAxisId') !== series.persistedConfig.yAxisId) {
series.set('yAxisId', series.persistedConfig.yAxisId);
}
}
}, this);
}

View File

@ -135,18 +135,44 @@ export default class YAxisModel extends Model {
}
}
resetStats() {
//TODO: do we need the series id here?
this.unset('stats');
this.seriesCollection.forEach(series => {
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
});
}
getSeriesForYAxis(seriesCollection) {
return seriesCollection.filter(series => {
const seriesYAxisId = series.get('yAxisId') || 1;
return seriesYAxisId === this.id;
});
}
getYAxisForId(id) {
const plotModel = this.plot.get('domainObject');
let yAxis;
if (this.id === 1) {
yAxis = plotModel.configuration?.yAxis;
} else {
if (plotModel.configuration?.additionalYAxes) {
yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id);
}
}
return yAxis;
}
/**
* @param {import('./PlotSeries').default} series
*/
trackSeries(series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (series.get('yAxisId') !== this.id) {
return;
}
if (!seriesStats) {
this.resetStats();
} else {
@ -154,8 +180,24 @@ export default class YAxisModel extends Model {
}
});
this.listenTo(series, 'change:yKey', () => {
if (series.get('yAxisId') !== this.id) {
return;
}
this.updateFromSeries(this.seriesCollection);
});
this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => {
if (oldYAxisId && this.id === oldYAxisId) {
this.resetStats();
this.updateFromSeries(this.seriesCollection);
}
if (series.get('yAxisId') === this.id) {
this.resetStats();
this.updateFromSeries(this.seriesCollection);
}
});
}
untrackSeries(series) {
this.stopListening(series);
@ -252,14 +294,40 @@ export default class YAxisModel extends Model {
// Update the series collection labels and formatting
this.updateFromSeries(this.seriesCollection);
}
/**
* For a given series collection, get the metadata of the current yKey for each series.
* Then return first available value of the given property from the metadata.
* @param {import('./SeriesCollection').default} series
* @param {String} property
*/
getMetadataValueByProperty(series, property) {
return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : ''))
.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
}
/**
* Update yAxis format, values, and label from known series.
* @param {import('./SeriesCollection').default} seriesCollection
*/
updateFromSeries(seriesCollection) {
const plotModel = this.plot.get('domainObject');
const label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first();
const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);
if (!seriesForThisYAxis.length) {
return;
}
const yAxis = this.getYAxisForId(this.id);
const label = yAxis?.label;
const sampleSeries = seriesForThisYAxis[0];
if (!sampleSeries || !sampleSeries.metadata) {
if (!label) {
this.unset('label');
@ -279,41 +347,17 @@ export default class YAxisModel extends Model {
}
this.set('values', yMetadata.values);
if (!label) {
const labelName = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');
if (labelName) {
this.set('label', labelName);
return;
}
const labelUnits = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
//if the name is not available, set the units as the label
const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');
if (labelUnits) {
this.set('label', labelUnits);
@ -331,7 +375,8 @@ export default class YAxisModel extends Model {
frozen: false,
autoscale: true,
logMode: options.model?.logMode ?? false,
autoscalePadding: 0.1
autoscalePadding: 0.1,
id: options.id
// 'range' is not specified here, it is undefined at first. When the
// user turns off autoscale, the current 'displayRange' is used for

View File

@ -36,20 +36,21 @@
/>
</ul>
<div
v-if="plotSeries.length"
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
>
<ul
v-if="!isStackedPlotObject"
v-for="(yAxis, index) in yAxesWithSeries"
:key="`yAxis-${index}`"
class="l-inspector-part js-yaxis-properties"
>
<h2 title="Y axis settings for this object">Y Axis</h2>
<h2 title="Y axis settings for this object">Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="Manually override how the Y axis is labeled."
>Label</div>
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
<div class="grid-cell value">{{ yAxis.label ? yAxis.label : "Not defined" }}</div>
</li>
<li class="grid-row">
<div
@ -57,7 +58,7 @@
title="Enable log mode."
>Log mode</div>
<div class="grid-cell value">
{{ logMode ? "Enabled" : "Disabled" }}
{{ yAxis.logMode ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
@ -66,32 +67,36 @@
title="Automatically scale the Y axis to keep all values in view."
>Auto scale</div>
<div class="grid-cell value">
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
{{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }}
</div>
</li>
<li
v-if="!autoscale && rangeMin"
v-if="!yAxis.autoscale && yAxis.rangeMin"
class="grid-row"
>
<div
class="grid-cell label"
title="Minimum Y axis value."
>Minimum value</div>
<div class="grid-cell value">{{ rangeMin }}</div>
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
</li>
<li
v-if="!autoscale && rangeMax"
v-if="!yAxis.autoscale && yAxis.rangeMax"
class="grid-row"
>
<div
class="grid-cell label"
title="Maximum Y axis value."
>Maximum value</div>
<div class="grid-cell value">{{ rangeMax }}</div>
<div class="grid-cell value">{{ yAxis.rangeMax }}</div>
</li>
</ul>
</div>
<div
v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
class="grid-properties"
>
<ul
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
class="l-inspector-part js-legend-properties"
>
<h2 title="Legend settings for this object">Legend</h2>
@ -157,12 +162,6 @@ export default {
data() {
return {
config: {},
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
@ -173,7 +172,8 @@ export default {
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
loaded: false,
plotSeries: []
plotSeries: [],
yAxes: []
};
},
computed: {
@ -181,14 +181,19 @@ export default {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
},
isStackedPlotObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
},
yAxesWithSeries() {
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.initYAxesConfiguration();
this.registerListeners();
this.initConfiguration();
this.initLegendConfiguration();
this.loaded = true;
},
@ -196,18 +201,38 @@ export default {
this.stopListening();
},
methods: {
initConfiguration() {
initYAxesConfiguration() {
if (this.config) {
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.logMode = this.config.yAxis.get('logMode');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
let range = this.config.yAxis.get('range');
this.yAxes.push({
id: this.config.yAxis.id,
seriesCount: 0,
label: this.config.yAxis.get('label'),
autoscale: this.config.yAxis.get('autoscale'),
logMode: this.config.yAxis.get('logMode'),
autoscalePadding: this.config.yAxis.get('autoscalePadding'),
rangeMin: range ? range.min : '',
rangeMax: range ? range.max : ''
});
this.config.additionalYAxes.forEach(yAxis => {
range = yAxis.get('range');
this.yAxes.push({
id: yAxis.id,
seriesCount: 0,
label: yAxis.get('label'),
autoscale: yAxis.get('autoscale'),
logMode: yAxis.get('logMode'),
autoscalePadding: yAxis.get('autoscalePadding'),
rangeMin: range ? range.min : '',
rangeMax: range ? range.max : ''
});
});
}
},
initLegendConfiguration() {
if (this.config) {
this.position = this.config.legend.get('position');
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.config.legend.get('expandByDefault');
@ -229,18 +254,44 @@ export default {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
}
},
setYAxisLabel(yAxisId) {
const found = this.yAxes.find(yAxis => yAxis.id === yAxisId);
if (found && found.seriesCount > 0) {
const mainYAxisId = this.config.yAxis.id;
if (mainYAxisId === yAxisId) {
found.label = this.config.yAxis.get('label');
} else {
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
if (additionalYAxis) {
found.label = additionalYAxis.get('label');
}
}
}
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, 1);
this.$set(this.plotSeries, index, series);
this.initConfiguration();
this.setYAxisLabel(yAxisId);
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
removeSeries(plotSeries, index) {
const yAxisId = plotSeries.get('yAxisId');
this.updateAxisUsageCount(yAxisId, -1);
this.plotSeries.splice(index, 1);
this.setYAxisLabel(yAxisId);
},
updateAxisUsageCount(yAxisId, updateCount) {
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
if (foundYAxis) {
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
}
}
}
};

View File

@ -40,8 +40,10 @@
</li>
</ul>
<y-axis-form
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
v-for="(yAxisId, index) in yAxesIds"
:id="yAxisId.id"
:key="`yAxis-${index}`"
class="grid-properties js-yaxis-grid-properties"
:y-axis="config.yAxis"
@seriesUpdated="updateSeriesConfigForObject"
/>
@ -76,21 +78,38 @@ export default {
data() {
return {
config: {},
yAxes: [],
plotSeries: [],
loaded: false
};
},
computed: {
isStackedPlotNestedObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
},
isStackedPlotObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
},
yAxesIds() {
return !this.isStackedPlotObject && this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.yAxes = [{
id: this.config.yAxis.id,
seriesCount: 0
}];
if (this.config.additionalYAxes) {
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
return {
id: yAxis.id,
seriesCount: 0
};
}));
}
this.registerListeners();
this.loaded = true;
},
@ -107,16 +126,47 @@ export default {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
},
findYAxisForId(yAxisId) {
return this.yAxes.find(yAxis => yAxis.id === yAxisId);
},
setYAxisLabel(yAxisId) {
const found = this.findYAxisForId(yAxisId);
if (found && found.seriesCount > 0) {
const mainYAxisId = this.config.yAxis.id;
if (mainYAxisId === yAxisId) {
found.label = this.config.yAxis.get('label');
} else {
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
if (additionalYAxis) {
found.label = additionalYAxis.get('label');
}
}
}
},
addSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, 1);
this.$set(this.plotSeries, index, series);
this.setYAxisLabel(yAxisId);
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
removeSeries(series, index) {
const yAxisId = series.get('yAxisId');
this.updateAxisUsageCount(yAxisId, -1);
this.plotSeries.splice(index, 1);
this.setYAxisLabel(yAxisId);
},
updateAxisUsageCount(yAxisId, updateCount) {
const foundYAxis = this.findYAxisForId(yAxisId);
if (foundYAxis) {
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
}
},
updateSeriesConfigForObject(config) {

View File

@ -1,7 +1,7 @@
<template>
<div>
<div v-if="loaded">
<ul class="l-inspector-part">
<h2>Y Axis</h2>
<h2>Y Axis {{ id > 1 ? id : '' }}</h2>
<li class="grid-row">
<div
class="grid-cell label"
@ -25,6 +25,7 @@
<!-- eslint-disable-next-line vue/html-self-closing -->
<input
v-model="logMode"
class="js-log-mode-input"
type="checkbox"
@change="updateForm('logMode')"
/>
@ -103,52 +104,72 @@
<script>
import { objectPath } from "./formUtil";
import _ from "lodash";
import eventHelpers from "../../lib/eventHelpers";
import configStore from "../../configuration/ConfigStore";
export default {
inject: ['openmct', 'domainObject'],
props: {
yAxis: {
type: Object,
default() {
return {};
}
id: {
type: Number,
required: true
}
},
data() {
return {
yAxis: null,
label: '',
autoscale: '',
logMode: false,
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
validationErrors: {}
validationErrors: {},
loaded: false
};
},
mounted() {
this.initialize();
eventHelpers.extend(this);
this.getConfig();
this.loaded = true;
this.initFields();
this.initFormValues();
},
methods: {
initialize: function () {
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const config = configStore.get(configId);
if (config) {
const mainYAxisId = config.yAxis.id;
this.isAdditionalYAxis = this.id !== mainYAxisId;
if (this.isAdditionalYAxis) {
this.additionalYAxes = config.additionalYAxes;
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
} else {
this.yAxis = config.yAxis;
}
}
},
initFields() {
const prefix = `configuration.${this.getPrefix()}`;
this.fields = {
label: {
objectPath: 'configuration.yAxis.label'
objectPath: `${prefix}.label`
},
autoscale: {
coerce: Boolean,
objectPath: 'configuration.yAxis.autoscale'
objectPath: `${prefix}.autoscale`
},
autoscalePadding: {
coerce: Number,
objectPath: 'configuration.yAxis.autoscalePadding'
objectPath: `${prefix}.autoscalePadding`
},
logMode: {
coerce: Boolean,
objectPath: 'configuration.yAxis.logMode'
objectPath: `${prefix}.logMode`
},
range: {
objectPath: 'configuration.yAxis.range',
objectPath: `${prefix}.range'`,
coerce: function coerceRange(range) {
const newRange = {
min: -1,
@ -202,6 +223,25 @@ export default {
this.rangeMin = range?.min;
this.rangeMax = range?.max;
},
getPrefix() {
let prefix = 'yAxis';
if (this.isAdditionalYAxis) {
let index = -1;
if (this.additionalYAxes) {
index = this.additionalYAxes.findIndex((yAxis) => {
return yAxis.id === this.id;
});
}
if (index < 0) {
index = 0;
}
prefix = `additionalYAxes[${index}]`;
}
return prefix;
},
updateForm(formKey) {
let newVal;
if (formKey === 'range') {
@ -231,18 +271,42 @@ export default {
this.yAxis.set(formKey, newVal);
// Then we mutate the domain object configuration to persist the settings
if (path) {
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
path: `yAxis.${formKey}`,
value: newVal
});
if (this.isAdditionalYAxis) {
if (this.domainObject.configuration && this.domainObject.configuration.series) {
//update the id
this.openmct.objects.mutate(
this.domainObject,
`configuration.${this.getPrefix()}.id`,
this.id
);
//update the yAxes values
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
} else {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
path: `${this.getPrefix()}.${formKey}`,
id: this.id,
value: newVal
});
}
} else {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
if (this.domainObject.configuration && this.domainObject.configuration.series) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
} else {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
path: `${this.getPrefix()}.${formKey}`,
value: newVal
});
}
}
}
}

View File

@ -0,0 +1,504 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 Plot from "../Plot.vue";
import configStore from "../configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "../inspector/PlotOptions.vue";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
let overlayPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.overlay",
name: "Test Overlay Plot",
composition: [],
configuration: {
series: []
}
};
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1',
'some-key2': 'some-value2 1',
'some-other-key2': 'some-other-value2 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2',
'some-key2': 'some-value2 2',
'some-other-key2': 'some-other-value2 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3',
'some-key2': 'some-value2 2',
'some-other-key2': 'some-other-value2 2'
}
];
const timeSystem = {
timeSystemKey: 'utc',
bounds: {
start: 0,
end: 4
}
};
openmct = createOpenMct(timeSystem);
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);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.types.addType("test-object", {
creatable: true
});
spyOnBuiltins(["requestAnimationFrame"]);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.router.path = [overlayPlotObject];
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
configStore.deleteAll();
resetApplicationState(openmct).then(done).catch(done);
});
afterAll(() => {
openmct.router.path = null;
});
describe("the plot views", () => {
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, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
expect(plotView).toBeDefined();
});
});
describe("The overlay plot view with multiple axes", () => {
let testTelemetryObject;
let testTelemetryObject2;
let config;
let component;
let mockComposition;
afterAll(() => {
component.$destroy();
openmct.router.path = null;
});
beforeEach(() => {
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
}
}]
}
};
overlayPlotObject.composition = [
{
identifier: testTelemetryObject.identifier
},
{
identifier: testTelemetryObject2.identifier
}
];
overlayPlotObject.configuration.series = [
{
identifier: testTelemetryObject.identifier,
yAxisId: 1
},
{
identifier: testTelemetryObject2.identifier,
yAxisId: 3
}
];
overlayPlotObject.configuration.additionalYAxes = [
{
label: 'Test Object Label',
id: 2
},
{
label: 'Test Object 2 Label',
id: 3
}
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
mockComposition.emit('add', testTelemetryObject2);
return [testTelemetryObject, testTelemetryObject2];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
Plot
},
provide: {
openmct: openmct,
domainObject: overlayPlotObject,
composition: openmct.composition.get(overlayPlotObject),
path: [overlayPlotObject]
},
template: '<plot ref="plotComponent"></plot>'
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
config = configStore.get(configId);
});
});
it("Renders multiple Y-axis for the telemetry objects", (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(2);
done();
});
});
describe('the inspector view', () => {
let inspectorComponent;
let viewComponentObject;
let selection;
beforeEach((done) => {
selection = [
[
{
context: {
item: {
id: overlayPlotObject.identifier.key,
identifier: overlayPlotObject.identifier,
type: overlayPlotObject.type,
configuration: overlayPlotObject.configuration,
composition: overlayPlotObject.composition
}
}
}
]
];
let viewContainer = document.createElement('div');
child.append(viewContainer);
inspectorComponent = new Vue({
el: viewContainer,
components: {
PlotOptions
},
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item]
},
template: '<plot-options/>'
});
Vue.nextTick(() => {
viewComponentObject = inspectorComponent.$root.$children[0];
done();
});
});
afterEach(() => {
openmct.router.path = null;
});
describe('in edit mode', () => {
let editOptionsEl;
beforeEach((done) => {
viewComponentObject.setEditState(true);
Vue.nextTick(() => {
editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
done();
});
});
it('shows multiple yAxis options', () => {
const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2");
expect(yAxisProperties.length).toEqual(2);
});
it('saves yAxis options', () => {
//toggle log mode and save
config.additionalYAxes[1].set('displayRange', {
min: 10,
max: 20
});
const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input");
const clickEvent = createMouseEvent("click");
yAxisProperties[1].dispatchEvent(clickEvent);
expect(config.additionalYAxes[1].get('logMode')).toEqual(true);
});
});
});
});
describe("The overlay plot view with single axes", () => {
let testTelemetryObject;
let config;
let component;
let mockComposition;
afterAll(() => {
component.$destroy();
openmct.router.path = null;
});
beforeEach(() => {
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
}
}]
}
};
overlayPlotObject.composition = [
{
identifier: testTelemetryObject.identifier
}
];
overlayPlotObject.configuration.series = [
{
identifier: testTelemetryObject.identifier
}
];
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: {
Plot
},
provide: {
openmct: openmct,
domainObject: overlayPlotObject,
composition: openmct.composition.get(overlayPlotObject),
path: [overlayPlotObject]
},
template: '<plot ref="plotComponent"></plot>'
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
config = configStore.get(configId);
});
});
it("Renders single Y-axis 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);
done();
});
});
});
});

View File

@ -28,6 +28,8 @@ import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
const TEST_KEY_ID = 'test-key';
describe("the plugin", function () {
let element;
let child;
@ -404,6 +406,20 @@ describe("the plugin", function () {
expect(options[1].value).toBe("Another attribute");
});
it("Updates the Y-axis label when changed", () => {
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
const config = configStore.get(configId);
const yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y")[0].__vue__;
config.yAxis.seriesCollection.models.forEach((plotSeries) => {
expect(plotSeries.model.yKey).toBe('some-key');
});
yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1);
config.yAxis.seriesCollection.models.forEach((plotSeries) => {
expect(plotSeries.model.yKey).toBe(TEST_KEY_ID);
});
});
it('hides the pause and play controls', () => {
let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right");

View File

@ -83,8 +83,6 @@ export default {
}
},
data() {
this.seriesConfig = {};
return {
hideExportButtons: false,
cursorGuide: false,
@ -121,6 +119,7 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.seriesConfig = {};
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.config = this.getConfig(configId);

View File

@ -468,15 +468,21 @@ describe("the plugin", function () {
});
it("updates the yscale", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[1].yScale.domain()).toEqual({
const yAxisList = [config.yAxis, ...config.additionalYAxes];
yAxisList.forEach((yAxis) => {
yAxis.set('displayRange', {
min: 10,
max: 20
});
});
Vue.nextTick(() => {
const yAxesScales = plotViewComponentObject.$children[1].component.$children[1].yScale;
yAxesScales.forEach((yAxisScale) => {
expect(yAxisScale.scale.domain()).toEqual({
min: 10,
max: 20
});
});
done();
});
});

View File

@ -59,6 +59,7 @@
.icon-database-in-brackets { @include glyphBefore($glyph-icon-database-in-brackets); }
.icon-eye-open { @include glyphBefore($glyph-icon-eye-open); }
.icon-gear { @include glyphBefore($glyph-icon-gear); }
.icon-gear-after { @include glyphAfter($glyph-icon-gear); }
.icon-hourglass { @include glyphBefore($glyph-icon-hourglass); }
.icon-info { @include glyphBefore($glyph-icon-info); }
.icon-link { @include glyphBefore($glyph-icon-link); }

View File

@ -244,16 +244,46 @@ mct-plot {
&.gl-plot-y-label {
display: block;
left: 0; top: 0; right: auto; bottom: 0;
left: 0; right: auto; bottom: 0;
text-orientation: mixed;
writing-mode: vertical-lr;
&:before {
//z-index allows clicking on visibility icon
z-index: 2;
.icon-gear-after:after {
// Icon denoting configurability
margin-top: $interiorMargin; // Uses margin-top due to writing-mode
}
.icon-eye-open:before, .icon-eye-disabled:before {
padding-top: 5px;
}
.plot-series-color-swatch {
@include colorSwatch();
display: inline-block;
flex: 0 0 auto;
margin-bottom: $interiorMargin; // Uses margin-bottom due to writing-mode
}
}
}
.plot-yaxis-right {
&.gl-plot-y { margin-left: 100%; }
.gl-plot-label {
&.gl-plot-y-label {
left: auto;
right: 0;
}
}
.gl-plot-y-label__select {
left: 0;
right: auto;
}
}
.gl-plot-x-label__select {
position: absolute;
left: 50%;
@ -264,7 +294,7 @@ mct-plot {
.gl-plot-y-label__select {
position: absolute;
top: 50%;
bottom: 2%;
transform: translateY(-50%);
left: 0;
z-index: 10;
@ -409,6 +439,14 @@ mct-plot {
}
}
.plot-yaxis-right {
.gl-plot-tick {
&.gl-plot-y-tick-label {
left: $interiorMarginSm;
right: auto;
}
}
}
.tick-label {
&.tick-label-x {
top: 0;

View File

@ -25,7 +25,7 @@
draggable="true"
@dragstart="emitDragStartEvent"
@dragenter="onDragenter"
@dragover="onDragover"
@dragover.prevent
@dragleave="onDragleave"
@drop="emitDropEvent"
>
@ -93,11 +93,8 @@ export default {
};
},
methods: {
onDragover(event) {
event.preventDefault();
},
emitDropEvent(event) {
this.$emit('drop-custom', this.index);
this.$emit('drop-custom', event);
this.hover = false;
},
emitDragStartEvent(event) {

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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-elements-pool__group"
:class="{
'hover': hover
}"
:allow-drop="allowDrop"
@dragover.prevent
@dragenter="onDragEnter"
@dragleave.stop="onDragLeave"
@drop="emitDrop"
>
<ul>
<div>
<span class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"></span>
<div
class="c-tree__item__type-icon c-object-label__type-icon"
>
<span
class="is-status__indicator"
></span>
</div>
<div
class="c-tree__item__name c-object-label__name"
:aria-label="`Element Item Group ${label}`"
>
{{ label }}
</div>
</div>
<slot></slot>
</ul>
</div>
</template>
<script>
export default {
props: {
parentObject: {
type: Object,
required: true,
default: () => {
return {};
}
},
label: {
type: String,
required: true,
default: () => {
return '';
}
},
allowDrop: {
type: Boolean
}
},
data() {
return {
dragCounter: 0
};
},
computed: {
hover() {
return this.dragCounter > 0;
}
},
methods: {
emitDrop(event) {
this.dragCounter = 0;
this.$emit('drop-group', event);
},
onDragEnter(event) {
this.dragCounter++;
},
onDragLeave(event) {
this.dragCounter--;
}
}
};
</script>

View File

@ -65,8 +65,8 @@ import ElementItem from './ElementItem.vue';
export default {
components: {
'Search': Search,
'ElementItem': ElementItem
Search,
ElementItem
},
inject: ['openmct'],
data() {

View File

@ -57,7 +57,12 @@
handle="before"
label="Elements"
>
<elements-pool />
<plot-elements-pool
v-if="isOverlayPlot"
/>
<elements-pool
v-else
/>
</pane>
</multipane>
<multipane
@ -91,12 +96,13 @@
</template>
<script>
import multipane from "../layout/multipane.vue";
import pane from "../layout/pane.vue";
import ElementsPool from "./ElementsPool.vue";
import Properties from "./details/Properties.vue";
import ObjectName from "./ObjectName.vue";
import InspectorViews from "./InspectorViews.vue";
import multipane from '../layout/multipane.vue';
import pane from '../layout/pane.vue';
import ElementsPool from './ElementsPool.vue';
import PlotElementsPool from './PlotElementsPool.vue';
import Properties from './details/Properties.vue';
import ObjectName from './ObjectName.vue';
import InspectorViews from './InspectorViews.vue';
import _ from "lodash";
import stylesManager from "@/ui/inspector/styles/StylesManager";
import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
@ -111,6 +117,7 @@ export default {
multipane,
pane,
ElementsPool,
PlotElementsPool,
Properties,
ObjectName,
InspectorViews
@ -130,6 +137,7 @@ export default {
hasComposition: false,
multiSelect: false,
showStyles: false,
isOverlayPlot: false,
tabbedViews: [
{
key: "__properties",
@ -186,6 +194,7 @@ export default {
this.hasComposition = Boolean(
parentObject && this.openmct.composition.get(parentObject)
);
this.isOverlayPlot = selection[0][0].context.item.type === 'telemetry.plot.overlay';
}
},
refreshTabs(selection) {

View File

@ -0,0 +1,330 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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-elements-pool is-object-type-telemetry-plot-overlay">
<Search
class="c-elements-pool__search"
:value="currentSearch"
@input="applySearch"
@clear="applySearch"
/>
<div
class="c-elements-pool__elements"
>
<ul
v-if="hasElements"
id="inspector-elements-tree"
class="c-tree c-elements-pool__tree"
>
<div class="c-elements-pool__instructions"> Select and drag an element to move it into a different axis. </div>
<element-item-group
v-for="(yAxis, index) in yAxes"
:key="`element-group-yaxis-${yAxis.id}`"
:parent-object="parentObject"
:allow-drop="allowDrop"
:label="`Y Axis ${yAxis.id}`"
@drop-group="moveTo($event, 0, yAxis.id)"
>
<li
class="js-first-place"
@drop="moveTo($event, 0, yAxis.id)"
></li>
<element-item
v-for="(element, elemIndex) in yAxis.elements"
:key="element.identifier.key"
:index="elemIndex"
:element-object="element"
:parent-object="parentObject"
:allow-drop="allowDrop"
@dragstart-custom="moveFrom($event, yAxis.id)"
@drop-custom="moveTo($event, index, yAxis.id)"
/>
<li
v-if="yAxis.elements.length > 0"
class="js-last-place"
@drop="moveTo($event, yAxis.elements.length, yAxis.id)"
></li>
</element-item-group>
</ul>
<div
v-if="!hasElements"
>
No contained elements
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash';
import Search from '../components/search.vue';
import ElementItem from './ElementItem.vue';
import ElementItemGroup from './ElementItemGroup.vue';
import configStore from '../../plugins/plot/configuration/ConfigStore';
const Y_AXIS_1 = 1;
export default {
components: {
Search,
ElementItemGroup,
ElementItem
},
inject: ['openmct'],
data() {
return {
yAxes: [],
isEditing: this.openmct.editor.isEditing(),
parentObject: undefined,
currentSearch: '',
selection: [],
contextClickTracker: {},
allowDrop: false
};
},
computed: {
hasElements() {
for (const yAxis of this.yAxes) {
if (yAxis.elements.length > 0) {
return true;
}
}
return false;
}
},
mounted() {
const selection = this.openmct.selection.get();
if (selection && selection.length > 0) {
this.showSelection(selection);
}
this.openmct.selection.on('change', this.showSelection);
this.openmct.editor.on('isEditing', this.setEditState);
},
destroyed() {
this.openmct.editor.off('isEditing', this.setEditState);
this.openmct.selection.off('change', this.showSelection);
this.unlistenComposition();
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
this.showSelection(this.openmct.selection.get());
},
showSelection(selection) {
if (_.isEqual(this.selection, selection)) {
return;
}
this.selection = selection;
this.elementsCache = {};
this.listeners = [];
this.parentObject = selection && selection[0] && selection[0][0].context.item;
this.unlistenComposition();
if (this.parentObject) {
this.setYAxisIds();
this.composition = this.openmct.composition.get(this.parentObject);
if (this.composition) {
this.composition.load();
this.registerCompositionListeners();
}
}
},
unlistenComposition() {
if (this.compositionUnlistener) {
this.compositionUnlistener();
}
},
registerCompositionListeners() {
this.composition.on('add', this.addElement);
this.composition.on('remove', this.removeElement);
this.composition.on('reorder', this.reorderElements);
this.compositionUnlistener = () => {
this.composition.off('add', this.addElement);
this.composition.off('remove', this.removeElement);
this.composition.off('reorder', this.reorderElements);
delete this.compositionUnlistener;
};
},
setYAxisIds() {
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
this.config = configStore.get(configId);
this.yAxes.push({
id: this.config.yAxis.id,
elements: this.parentObject.configuration.series.filter(
series => series.yAxisId === this.config.yAxis.id
)
});
if (this.config.additionalYAxes) {
this.config.additionalYAxes.forEach(yAxis => {
this.yAxes.push({
id: yAxis.id,
elements: this.parentObject.configuration.series.filter(
series => series.yAxisId === yAxis.id
)
});
});
}
},
addElement(element) {
// Get the index of the corresponding element in the series list
const seriesIndex = this.parentObject.configuration.series.findIndex(
series => this.openmct.objects.areIdsEqual(series.identifier, element.identifier)
);
const keyString = this.openmct.objects.makeKeyString(element.identifier);
const wasDraggedOntoPlot = this.parentObject.configuration.series[seriesIndex].yAxisId === undefined;
const yAxisId = wasDraggedOntoPlot
? Y_AXIS_1
: this.parentObject.configuration.series[seriesIndex].yAxisId;
if (wasDraggedOntoPlot) {
const insertIndex = this.yAxes[0].elements.length;
// Insert the element at the end of the first YAxis bucket
this.composition.reorder(seriesIndex, insertIndex);
}
// Store the element in the cache and set its yAxisId
this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));
if (this.elementsCache[keyString].yAxisId !== yAxisId) {
// Mutate the YAxisId on the domainObject itself
this.updateCacheAndMutate(element, yAxisId);
}
this.applySearch(this.currentSearch);
},
reorderElements() {
this.applySearch(this.currentSearch);
},
removeElement(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
delete this.elementsCache[keyString];
this.applySearch(this.currentSearch);
},
applySearch(input) {
this.currentSearch = input;
this.yAxes.forEach(yAxis => {
yAxis.elements = this.filterForSearchAndAxis(input, yAxis.id);
});
},
filterForSearchAndAxis(input, yAxisId) {
return this.parentObject.composition.map((id) =>
this.elementsCache[this.openmct.objects.makeKeyString(id)]
).filter((element) => {
return element !== undefined
&& element.name.toLowerCase().search(input) !== -1
&& element.yAxisId === yAxisId;
});
},
moveFrom(elementIndex, groupIndex) {
this.allowDrop = true;
this.moveFromIndex = elementIndex;
this.moveFromYAxisId = groupIndex;
},
moveTo(event, moveToIndex, moveToYAxisId) {
// FIXME: If the user starts the drag by clicking outside of the <object-label/> element,
// domain object information will not be set on the dataTransfer data. To prevent errors,
// we simply short-circuit here if the data is not set.
const serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object');
if (!serializedDomainObject) {
return;
}
const domainObject = JSON.parse(serializedDomainObject);
this.updateCacheAndMutate(domainObject, moveToYAxisId);
const moveFromIndex = this.moveFromIndex;
this.moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId);
},
updateCacheAndMutate(domainObject, yAxisId) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const index = this.parentObject.configuration.series.findIndex(
series => series.identifier.key === domainObject.identifier.key
);
// Handle the case of dragging an element directly into the Elements Pool
if (!this.elementsCache[keyString]) {
// Update the series list locally so our CompositionAdd handler can
// take care of the rest.
this.parentObject.configuration.series.push({
identifier: domainObject.identifier,
yAxisId
});
this.composition.add(domainObject);
this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject));
}
this.elementsCache[keyString].yAxisId = yAxisId;
const shouldMutate = this.parentObject.configuration.series?.[index]?.yAxisId !== yAxisId;
if (shouldMutate) {
this.openmct.objects.mutate(
this.parentObject,
`configuration.series[${index}].yAxisId`,
yAxisId
);
}
},
moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId) {
if (!this.allowDrop) {
return;
}
// Find the corresponding indexes of the from/to yAxes in the yAxes list
const moveFromYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === this.moveFromYAxisId);
const moveToYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === moveToYAxisId);
// Calculate the actual indexes of the elements in the composition array
// based on which bucket and index they are being moved from/to.
// Then, trigger a composition reorder.
for (let yAxisId = 0; yAxisId < moveFromYAxisIndex; yAxisId++) {
const lesserYAxisBucketLength = this.yAxes[yAxisId].elements.length;
// Add the lengths of preceding buckets to calculate the actual 'from' index
moveFromIndex = moveFromIndex + lesserYAxisBucketLength;
}
for (let yAxisId = 0; yAxisId < moveToYAxisIndex; yAxisId++) {
const greaterYAxisBucketLength = this.yAxes[yAxisId].elements.length;
// Add the lengths of subsequent buckets to calculate the actual 'to' index
moveToIndex = moveToIndex + greaterYAxisBucketLength;
}
// Adjust the index by 1 if we're moving from one bucket to another
if (this.moveFromYAxisId !== moveToYAxisId && moveToIndex > 0) {
moveToIndex--;
}
// Reorder the composition array according to the calculated indexes
this.composition.reorder(moveFromIndex, moveToIndex);
this.allowDrop = false;
}
}
};
</script>

View File

@ -3,11 +3,27 @@
flex-direction: column;
overflow: hidden;
flex: 1 1 auto !important;
> * + * {
margin-top: $interiorMargin;
}
&.is-object-type-telemetry-plot-overlay {
.c-grippy {
display: none;
}
.c-object-label{
&:before {
// Grippy
content: '';
@include grippy($colorItemTreeVC, $dir: 'Y');
$d: 9px;
width: $d; height: $d;
display: block;
margin-right: $interiorMargin;
}
}
}
&__item {
&.is-alias {
// Object is an alias to an original.
@ -21,11 +37,22 @@
flex: 0 0 auto;
}
&__group {
flex: 1 1 auto;
overflow: auto;
margin-top: $interiorMarginLg;
}
&__elements {
flex: 1 1 auto;
overflow: auto;
}
&__instructions {
display: flex;
font-style: italic;
}
.c-grippy {
$d: 9px;
flex: 0 0 auto;