Compare commits
7 Commits
eslint-pla
...
hide-gripp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b117890f27 | ||
|
|
afc37209d2 | ||
|
|
e45b58e2a8 | ||
|
|
8b3487bdbe | ||
|
|
50719b1383 | ||
|
|
cfda067794 | ||
|
|
4847cc8931 |
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
116
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
116
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
|
||||
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group"]').nth(2));
|
||||
const elementsTree = await page.locator('#inspector-elements-tree').allInnerTexts();
|
||||
expect(elementsTree.join('').split('\n')).toEqual([
|
||||
"Y Axis 1",
|
||||
"swg d",
|
||||
"Y Axis 2",
|
||||
"swg e",
|
||||
"swg c",
|
||||
"swg a",
|
||||
"Y Axis 3",
|
||||
"swg b"
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -34,23 +34,26 @@
|
||||
@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-${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"
|
||||
/>
|
||||
</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 +72,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"
|
||||
/>
|
||||
|
||||
@@ -214,6 +220,7 @@ import YAxis from "./axis/YAxis.vue";
|
||||
import _ from "lodash";
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
const AXES_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -269,7 +276,6 @@ export default {
|
||||
altPressed: false,
|
||||
highlights: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
yKeyOptions: [],
|
||||
yAxisLabel: '',
|
||||
rectangles: [],
|
||||
@@ -284,12 +290,31 @@ export default {
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: this.limitLineLabels,
|
||||
isFrozenOnMouseDown: false,
|
||||
hasSameRangeValue: true,
|
||||
cursorGuide: this.initCursorGuide,
|
||||
gridLines: this.initGridLines
|
||||
gridLines: this.initGridLines,
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
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));
|
||||
|
||||
@@ -312,8 +337,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: {
|
||||
@@ -342,6 +376,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
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.isNestedWithinAStackedPlot) {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
@@ -383,8 +431,10 @@ export default {
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateDisplayBounds(this.timeContext.bounds());
|
||||
@@ -417,12 +467,13 @@ 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);
|
||||
|
||||
@@ -430,20 +481,21 @@ export default {
|
||||
this.loadSeriesData(series);
|
||||
}, 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);
|
||||
updateAxisUsageCount(yAxisId, updateCountBy) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
|
||||
}
|
||||
},
|
||||
|
||||
loadSeriesData(series) {
|
||||
@@ -673,6 +725,7 @@ export default {
|
||||
|
||||
// Setup canvas etc.
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
//TODO: handle yScale, zoom/pan for all yAxes
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
|
||||
this.pan = undefined;
|
||||
@@ -690,6 +743,9 @@ export default {
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
});
|
||||
},
|
||||
|
||||
onXAxisChange(displayBounds) {
|
||||
@@ -704,20 +760,24 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
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.tickWidth, id);
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
|
||||
}
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
@@ -1108,8 +1168,9 @@ export default {
|
||||
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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@
|
||||
<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
|
||||
@@ -52,6 +50,7 @@
|
||||
</select>
|
||||
|
||||
<mct-ticks
|
||||
:axis-id="id"
|
||||
:axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
:position="'top'"
|
||||
@@ -63,6 +62,10 @@
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
|
||||
const AXIS_PADDING = 20;
|
||||
const AXIS_OFFSET = 5;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -70,22 +73,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 +84,132 @@ export default {
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
plotLeftTickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
multipleLeftAxes: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'left';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.seriesModels = [];
|
||||
|
||||
return {
|
||||
yAxisLabel: 'none',
|
||||
loaded: false
|
||||
loaded: false,
|
||||
yKeyOptions: [],
|
||||
hasSameRangeValue: true,
|
||||
singleSeries: true,
|
||||
mainYAxisId: null,
|
||||
hasAdditionalYAxes: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
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 = `${ this.plotLeftTickWidth - this.tickWidth - multipleAxesPadding - AXIS_OFFSET }px`;
|
||||
style['border-right'] = `1px solid`;
|
||||
} else {
|
||||
style.left = `${this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
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 = this.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.checkRangeValueAndSingleSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
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.checkRangeValueAndSingleSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
checkRangeValueAndSingleSeries() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
this.singleSeries = this.seriesModels.length === 1;
|
||||
},
|
||||
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 +221,22 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,7 +98,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.resetOffsetAndSeriesDataForYAxis.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(this.config.yAxis, 'change:key', this.resetOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
});
|
||||
}
|
||||
|
||||
this.seriesElements = new WeakMap();
|
||||
this.seriesLimits = new WeakMap();
|
||||
|
||||
@@ -111,8 +125,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');
|
||||
@@ -224,25 +237,31 @@ 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) {
|
||||
resetOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].x;
|
||||
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;
|
||||
|
||||
const lines = this.lines.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
lines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.limitLines.forEach(function (line) {
|
||||
const limitLines = this.limitLines.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
limitLines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.pointSets.forEach(function (pointSet) {
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisId.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;
|
||||
}
|
||||
|
||||
@@ -251,19 +270,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;
|
||||
@@ -311,11 +331,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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,33 +347,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
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -410,8 +446,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;
|
||||
}
|
||||
|
||||
@@ -434,16 +470,31 @@ 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateViewport() {
|
||||
updateViewport(yAxisId) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.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');
|
||||
}
|
||||
}
|
||||
|
||||
if (!xRange || !yRange) {
|
||||
return;
|
||||
@@ -454,9 +505,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(
|
||||
@@ -464,38 +516,66 @@ export default {
|
||||
origin
|
||||
);
|
||||
},
|
||||
drawSeries() {
|
||||
this.lines.forEach(this.drawLine, this);
|
||||
this.pointSets.forEach(this.drawPoints, this);
|
||||
this.alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
matchByYAxisId(id, item) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
let matchesId = false;
|
||||
|
||||
const series = item.series;
|
||||
if (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
|
||||
@@ -577,22 +657,25 @@ export default {
|
||||
);
|
||||
},
|
||||
drawLine(chartElement, disconnected) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
},
|
||||
drawHighlights() {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
if (chartElement) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
}
|
||||
},
|
||||
drawHighlight(highlight) {
|
||||
drawHighlights(yAxisId) {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
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();
|
||||
@@ -601,20 +684,21 @@ export default {
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
|
||||
},
|
||||
drawRectangles() {
|
||||
drawRectangles(yAxisId) {
|
||||
if (this.rectangles) {
|
||||
this.rectangles.forEach(this.drawRectangle, this);
|
||||
const rectangles = this.rectangles.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawRectangle(rect) {
|
||||
drawRectangle(yAxisId, rect) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset.x(rect.start.x),
|
||||
this.offset.y(rect.start.y)
|
||||
this.offset[yAxisId].x(rect.start.x),
|
||||
this.offset[yAxisId].y(rect.start.y)
|
||||
],
|
||||
[
|
||||
this.offset.x(rect.end.x),
|
||||
this.offset.y(rect.end.y)
|
||||
this.offset[yAxisId].x(rect.end.x),
|
||||
this.offset[yAxisId].y(rect.end.y)
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
|
||||
@@ -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 indiidual 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 ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +180,10 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
});
|
||||
}
|
||||
@@ -252,14 +282,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 +335,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 +363,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
|
||||
|
||||
@@ -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 {{ 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: {
|
||||
@@ -182,13 +182,18 @@ export default {
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,6 +78,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
yAxes: [],
|
||||
plotSeries: [],
|
||||
loaded: false
|
||||
};
|
||||
@@ -86,11 +89,27 @@ export default {
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loaded">
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis</h2>
|
||||
<h2>Y Axis {{ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
504
src/plugins/plot/overlayPlot/pluginSpec.js
Normal file
504
src/plugins/plot/overlayPlot/pluginSpec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -593,6 +593,8 @@ mct-plot {
|
||||
.plot-legend-left .gl-plot-legend { margin-right: $interiorMargin; }
|
||||
.plot-legend-right .gl-plot-legend { margin-left: $interiorMargin; }
|
||||
|
||||
.gl-plot .plot-yaxis-right.gl-plot-y { margin-left: 100%; }
|
||||
|
||||
.gl-plot,
|
||||
.c-plot {
|
||||
&.plot-legend-collapsed .plot-wrapper-expanded-legend { display: none; }
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
draggable="true"
|
||||
@dragstart="emitDragStartEvent"
|
||||
@dragenter="onDragenter"
|
||||
@dragover="onDragover"
|
||||
@dragover.prevent
|
||||
@dragleave="onDragleave"
|
||||
@drop="emitDropEvent"
|
||||
>
|
||||
@@ -38,6 +38,7 @@
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="showGrippy"
|
||||
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
|
||||
></span>
|
||||
<object-label
|
||||
@@ -81,6 +82,10 @@ export default {
|
||||
},
|
||||
allowDrop: {
|
||||
type: Boolean
|
||||
},
|
||||
showGrippy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -93,11 +98,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) {
|
||||
|
||||
101
src/ui/inspector/ElementItemGroup.vue
Normal file
101
src/ui/inspector/ElementItemGroup.vue
Normal 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 }}
|
||||
</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>
|
||||
@@ -65,8 +65,8 @@ import ElementItem from './ElementItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'Search': Search,
|
||||
'ElementItem': ElementItem
|
||||
Search,
|
||||
ElementItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
|
||||
@@ -56,7 +56,12 @@
|
||||
handle="before"
|
||||
label="Elements"
|
||||
>
|
||||
<elements-pool />
|
||||
<plot-elements-pool
|
||||
v-if="isOverlayPlot"
|
||||
/>
|
||||
<elements-pool
|
||||
v-else
|
||||
/>
|
||||
</pane>
|
||||
</multipane>
|
||||
<multipane
|
||||
@@ -83,6 +88,7 @@
|
||||
import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import PlotElementsPool from './PlotElementsPool.vue';
|
||||
import Location from './Location.vue';
|
||||
import Properties from './details/Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
@@ -99,6 +105,7 @@ export default {
|
||||
multipane,
|
||||
pane,
|
||||
ElementsPool,
|
||||
PlotElementsPool,
|
||||
Properties,
|
||||
ObjectName,
|
||||
Location,
|
||||
@@ -118,6 +125,7 @@ export default {
|
||||
return {
|
||||
hasComposition: false,
|
||||
showStyles: false,
|
||||
isOverlayPlot: false,
|
||||
tabbedViews: [{
|
||||
key: '__properties',
|
||||
name: 'Properties'
|
||||
@@ -151,6 +159,7 @@ export default {
|
||||
let parentObject = selection[0][0].context.item;
|
||||
|
||||
this.hasComposition = Boolean(parentObject && this.openmct.composition.get(parentObject));
|
||||
this.isOverlayPlot = selection[0][0].context.item.type === 'telemetry.plot.overlay';
|
||||
}
|
||||
},
|
||||
refreshTabs(selection) {
|
||||
|
||||
330
src/ui/inspector/PlotElementsPool.vue
Normal file
330
src/ui/inspector/PlotElementsPool.vue
Normal 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">
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
:show-grippy="false"
|
||||
@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>
|
||||
@@ -21,6 +21,11 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__group {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__elements {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
Reference in New Issue
Block a user