Compare commits

...

7 Commits

Author SHA1 Message Date
Jesse Mazzella
b117890f27 feat(ElementItem): optionally hide grippy 2023-01-17 20:53:45 -08:00
Shefali Joshi
afc37209d2 Allow customizing multiple y axes (#6095)
* Allow displaying and updating multiple y axes

* Fix label updates when series are added or removed

* Add tests for y axes configuration in the inspector

* Addresses review comments: Renames properties for clarity and reorganize code for readability.

* test: fix selector for snapshot test

* Fix e2e selectors

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-01-17 10:09:56 -08:00
Jesse Mazzella
e45b58e2a8 feat(MultiYAxis): allow drag/drop of series directly into YAxis buckets (#6099)
* feat: allow dragging series directly into yAxis buckets

* fix: insert into first YAxis elements list
2023-01-12 10:49:00 -08:00
Shefali Joshi
8b3487bdbe Fix y axes rendering and style for 3rd y axis (#6081)
* Update plot to render y axes based on which series are added to the y axes, also add style to render third axis to the right of plots main canvas

* Clean up hardcoded numbers for consts and use objects for styles

* Rename parameter for readability

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-29 13:50:58 -08:00
Jesse Mazzella
50719b1383 [Plot] Allow bucketing of plot series into different Y Axes (#5878)
* WIP

* [Plot] Set yKey on each plot series (#5790)

* Set yKey on each model

* Add a test

* WIP 2: Electric Boogaloo

* Add uuid to YAxisModel

* determine dragover status for element group

* styling for elements group

* Drag and drop between groups, WIP on applySearch

* fix error

* Persist YAxisId on composition

* update yAxisId in cache

* cleanup

* remove unused import

* remove console.log

* mutate yAxisId whether or not it existed already

* Use null coalescing operator

* Cleanup some more

- remove unused vars

- rename vars

- add comments

* Handle when domainObject info is not set on the drag event

* Use constants

* cleanup, fix reorder index logic

* remove unused uuid

* more cleanup, add some comments

* Rename test file

* Clean up existing e2e test with our new patterns

* Simplify code and fix off-by-one index problem

* Add aria-label to element group

* Add test for elements pool reordering

* refactor: remove empty components object

* Draft: add 3rd y axis to the right of plots

* Fix CSS for left and right y axes making them more generic

* refactor: move logic to method

* fix: calculated --> computed 🤦‍♂️

* fix(WIP): initial work for dynamic yAxes

* fix(WIP): generic logic for reordering elements

* fix: calculate from/to indexes correctly

* refactor: remove unused constants

* refactor: use `areIdsEqual` API method

* refactor: remove redundant check

* fix: restore code removed by accident

* Remove y-axes bug fixes from elements pool changes.

* fix: make v-for key more unique

* fix: code review comments

- Only mutate domainObject if yAxisId has changed

- Initialize elements with blank arrays

- Only call `setYAxisIds` if `parentObject` is not null

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
2022-12-27 16:02:48 -08:00
Shefali Joshi
cfda067794 Multiple y axes model changes for plots (#6026)
* Update version for sprint 2.1.1

* Basic changes (very draft) for multiple y axis for plots

* Generalize additional axes model handling

* [Plot] Set yKey on each plot series (#5790)

* Set yKey on each model

* Add a test

* Update yaxis to be independent of mctplot

* Fix yAxis ticks

* Fix yAxisModel label

* Add multiple y axes option for overlay plot. UI changes to display 2 y-axes

* Fix Chart bug

* Bump version to `2.1.3` (#5973)

* Preserve Gauge configuration changes on create/edit (#5986)

* fix(#5985): deep merge on create/edit properties

- Perform a deep merge of old and new properties on Create/Edit properties actions

* refactor(e2e): improve selector in appActions

* test(e2e): add tests for gauges

- test creating a non-default gauge (checks only for console errors)
- test updating a gauge (checks only for console errors)

* fix(e2e): use pluginFixtures for gauge tests

* fix(e2e): prevent fail if testNotes is undefined

* Make the tree key unique (#5989)

* Refactor iterating over all yAxis while drawing to the viewport

* Refactor code to remove hardcoding of yAxisIds

* style: auto-fix lint errors

* Remove configurability of number of y axes. Use yAxisIds from Series to show/hide axes.
Adds test for multiple y axis display

* Add more tests for when only 1 axis needs to be displayed

* Address code review comments:
Move styles to computed properties
Refactor yAxes rendering in MctPlot to also include main y axis.

* Address review comments. Refactor plot configuration code for additiona y axes and reset the chart only for series that belong to the yaxis that has changed.

* Fix bug where only the first series for a given yAxis was rendering

* Refactor code to reuse method logic, move const and use cap case

* Only draw highlights for the given yAxis

* Only draw rectangles for the yAxis the series belongs to

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-12-27 16:02:48 -08:00
Khalid Adil
4847cc8931 [Plot] Set yKey on each plot series (#5790)
* Set yKey on each model

* Add a test
2022-12-27 16:02:48 -08:00
22 changed files with 1843 additions and 275 deletions

View File

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

View File

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

View File

@@ -0,0 +1,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"
]);
});
});

View File

@@ -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() {

View File

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

View File

@@ -22,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
});
}
}
};

View File

@@ -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
);

View File

@@ -27,6 +27,10 @@ import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
const MAX_Y_AXES = 3;
const MAIN_Y_AXES_ID = 1;
const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The 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 ?? {})
};
}

View File

@@ -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
};
}

View File

@@ -135,18 +135,44 @@ export default class YAxisModel extends Model {
}
}
resetStats() {
//TODO: do we need the series id here?
this.unset('stats');
this.seriesCollection.forEach(series => {
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
});
}
getSeriesForYAxis(seriesCollection) {
return seriesCollection.filter(series => {
const seriesYAxisId = series.get('yAxisId') || 1;
return seriesYAxisId === this.id;
});
}
getYAxisForId(id) {
const plotModel = this.plot.get('domainObject');
let yAxis;
if (this.id === 1) {
yAxis = plotModel.configuration?.yAxis;
} else {
if (plotModel.configuration?.additionalYAxes) {
yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id);
}
}
return yAxis;
}
/**
* @param {import('./PlotSeries').default} series
*/
trackSeries(series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (series.get('yAxisId') !== this.id) {
return;
}
if (!seriesStats) {
this.resetStats();
} else {
@@ -154,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

View File

@@ -36,20 +36,21 @@
/>
</ul>
<div
v-if="plotSeries.length"
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
>
<ul
v-if="!isStackedPlotObject"
v-for="(yAxis, index) in yAxesWithSeries"
:key="`yAxis-${index}`"
class="l-inspector-part js-yaxis-properties"
>
<h2 title="Y axis settings for this object">Y Axis</h2>
<h2 title="Y axis settings for this object">Y Axis {{ 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;
}
}
}
};

View File

@@ -40,8 +40,10 @@
</li>
</ul>
<y-axis-form
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
v-for="(yAxisId, index) in yAxesIds"
:id="yAxisId.id"
:key="`yAxis-${index}`"
class="grid-properties js-yaxis-grid-properties"
:y-axis="config.yAxis"
@seriesUpdated="updateSeriesConfigForObject"
/>
@@ -76,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) {

View File

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

View File

@@ -0,0 +1,504 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "../plugin";
import Vue from "vue";
import Plot from "../Plot.vue";
import configStore from "../configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "../inspector/PlotOptions.vue";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
let overlayPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.overlay",
name: "Test Overlay Plot",
composition: [],
configuration: {
series: []
}
};
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1',
'some-key2': 'some-value2 1',
'some-other-key2': 'some-other-value2 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2',
'some-key2': 'some-value2 2',
'some-other-key2': 'some-other-value2 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3',
'some-key2': 'some-value2 2',
'some-other-key2': 'some-other-value2 2'
}
];
const timeSystem = {
timeSystemKey: 'utc',
bounds: {
start: 0,
end: 4
}
};
openmct = createOpenMct(timeSystem);
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new PlotVuePlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.types.addType("test-object", {
creatable: true
});
spyOnBuiltins(["requestAnimationFrame"]);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.router.path = [overlayPlotObject];
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
configStore.deleteAll();
resetApplicationState(openmct).then(done).catch(done);
});
afterAll(() => {
openmct.router.path = null;
});
describe("the plot views", () => {
it("provides an overlay plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.overlay",
telemetry: {
values: [{
key: "some-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
expect(plotView).toBeDefined();
});
});
describe("The overlay plot view with multiple axes", () => {
let testTelemetryObject;
let testTelemetryObject2;
let config;
let component;
let mockComposition;
afterAll(() => {
component.$destroy();
openmct.router.path = null;
});
beforeEach(() => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key2",
name: "Some attribute2",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
overlayPlotObject.composition = [
{
identifier: testTelemetryObject.identifier
},
{
identifier: testTelemetryObject2.identifier
}
];
overlayPlotObject.configuration.series = [
{
identifier: testTelemetryObject.identifier,
yAxisId: 1
},
{
identifier: testTelemetryObject2.identifier,
yAxisId: 3
}
];
overlayPlotObject.configuration.additionalYAxes = [
{
label: 'Test Object Label',
id: 2
},
{
label: 'Test Object 2 Label',
id: 3
}
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
mockComposition.emit('add', testTelemetryObject2);
return [testTelemetryObject, testTelemetryObject2];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
Plot
},
provide: {
openmct: openmct,
domainObject: overlayPlotObject,
composition: openmct.composition.get(overlayPlotObject),
path: [overlayPlotObject]
},
template: '<plot ref="plotComponent"></plot>'
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
config = configStore.get(configId);
});
});
it("Renders multiple Y-axis for the telemetry objects", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
expect(yAxisElement.length).toBe(2);
done();
});
});
describe('the inspector view', () => {
let inspectorComponent;
let viewComponentObject;
let selection;
beforeEach((done) => {
selection = [
[
{
context: {
item: {
id: overlayPlotObject.identifier.key,
identifier: overlayPlotObject.identifier,
type: overlayPlotObject.type,
configuration: overlayPlotObject.configuration,
composition: overlayPlotObject.composition
}
}
}
]
];
let viewContainer = document.createElement('div');
child.append(viewContainer);
inspectorComponent = new Vue({
el: viewContainer,
components: {
PlotOptions
},
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item]
},
template: '<plot-options/>'
});
Vue.nextTick(() => {
viewComponentObject = inspectorComponent.$root.$children[0];
done();
});
});
afterEach(() => {
openmct.router.path = null;
});
describe('in edit mode', () => {
let editOptionsEl;
beforeEach((done) => {
viewComponentObject.setEditState(true);
Vue.nextTick(() => {
editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
done();
});
});
it('shows multiple yAxis options', () => {
const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2");
expect(yAxisProperties.length).toEqual(2);
});
it('saves yAxis options', () => {
//toggle log mode and save
config.additionalYAxes[1].set('displayRange', {
min: 10,
max: 20
});
const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input");
const clickEvent = createMouseEvent("click");
yAxisProperties[1].dispatchEvent(clickEvent);
expect(config.additionalYAxes[1].get('logMode')).toEqual(true);
});
});
});
});
describe("The overlay plot view with single axes", () => {
let testTelemetryObject;
let config;
let component;
let mockComposition;
afterAll(() => {
component.$destroy();
openmct.router.path = null;
});
beforeEach(() => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
overlayPlotObject.composition = [
{
identifier: testTelemetryObject.identifier
}
];
overlayPlotObject.configuration.series = [
{
identifier: testTelemetryObject.identifier
}
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
Plot
},
provide: {
openmct: openmct,
domainObject: overlayPlotObject,
composition: openmct.composition.get(overlayPlotObject),
path: [overlayPlotObject]
},
template: '<plot ref="plotComponent"></plot>'
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier);
config = configStore.get(configId);
});
});
it("Renders single Y-axis for the telemetry object", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
expect(yAxisElement.length).toBe(1);
done();
});
});
});
});

View File

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

View File

@@ -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; }

View File

@@ -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) {

View File

@@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-elements-pool__group"
:class="{
'hover': hover
}"
:allow-drop="allowDrop"
@dragover.prevent
@dragenter="onDragEnter"
@dragleave.stop="onDragLeave"
@drop="emitDrop"
>
<ul>
<div>
<span class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"></span>
<div
class="c-tree__item__type-icon c-object-label__type-icon"
>
<span
class="is-status__indicator"
></span>
</div>
<div
class="c-tree__item__name c-object-label__name"
aria-label="Element Item Group"
>
{{ label }}
</div>
</div>
<slot></slot>
</ul>
</div>
</template>
<script>
export default {
props: {
parentObject: {
type: Object,
required: true,
default: () => {
return {};
}
},
label: {
type: String,
required: true,
default: () => {
return '';
}
},
allowDrop: {
type: Boolean
}
},
data() {
return {
dragCounter: 0
};
},
computed: {
hover() {
return this.dragCounter > 0;
}
},
methods: {
emitDrop(event) {
this.dragCounter = 0;
this.$emit('drop-group', event);
},
onDragEnter(event) {
this.dragCounter++;
},
onDragLeave(event) {
this.dragCounter--;
}
}
};
</script>

View File

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

View File

@@ -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) {

View File

@@ -0,0 +1,330 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-elements-pool">
<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>

View File

@@ -21,6 +21,11 @@
flex: 0 0 auto;
}
&__group {
flex: 1 1 auto;
overflow: auto;
}
&__elements {
flex: 1 1 auto;
overflow: auto;