mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 17:33:23 +00:00
Multiple Y-Axes for Overlay Plots (#6153)
Support multiple y-axes in overlay plots Co-authored-by: Khalid Adil <khalidadil29@gmail.com> Co-authored-by: Shefali Joshi <simplyrender@gmail.com> Co-authored-by: Rukmini Bose <rukmini.bose15@gmail.com>
This commit is contained in:
parent
9980aab18f
commit
1b71a3bf33
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
124
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
124
e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Overlay Plot', () => {
|
||||
test('Plot legend color is in sync with plot series color', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
|
||||
await page.locator('.c-click-swatch--menu').click();
|
||||
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
|
||||
|
||||
// gets color for swatch located in legend
|
||||
const element = await page.waitForSelector('.plot-series-color-swatch');
|
||||
const color = await element.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
});
|
||||
|
||||
expect(color).toBe('rgb(255, 166, 61)');
|
||||
});
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg a',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg b',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg c',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg d',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: 'swg e',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Expand the elements pool vertically
|
||||
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
// Drag swg a, c, e into Y Axis 2
|
||||
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
|
||||
// Drag swg b into Y Axis 3
|
||||
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
|
||||
|
||||
const yAxis1Group = page.getByLabel("Y Axis 1");
|
||||
const yAxis2Group = page.getByLabel("Y Axis 2");
|
||||
const yAxis3Group = page.getByLabel("Y Axis 3");
|
||||
|
||||
// Verify that the elements are in the correct buckets and in the correct order
|
||||
expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy();
|
||||
expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy();
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -34,23 +34,27 @@
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis
|
||||
v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:has-same-range-value="hasSameRangeValue"
|
||||
:series-model="seriesModels[0]"
|
||||
:style="{
|
||||
left: (plotWidth - tickWidth) + 'px'
|
||||
}"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
<div
|
||||
v-if="seriesModels.length"
|
||||
class="u-contents"
|
||||
>
|
||||
<y-axis
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
:id="yAxis.id"
|
||||
:key="`yAxis-${yAxis.id}-${index}`"
|
||||
:multiple-left-axes="multipleLeftAxes"
|
||||
:position="yAxis.id > 2 ? 'right' : 'left'"
|
||||
:class="{'plot-yaxis-right': yAxis.id > 2}"
|
||||
:tick-width="yAxis.tickWidth"
|
||||
:plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
@toggleAxisVisibility="toggleSeriesForYAxis"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-plot-wrapper-display-area-and-x-axis"
|
||||
:style="{
|
||||
left: (plotWidth + 20) + 'px'
|
||||
}"
|
||||
:style="xAxisStyle"
|
||||
>
|
||||
|
||||
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
|
||||
@ -69,9 +73,12 @@
|
||||
/>
|
||||
|
||||
<mct-ticks
|
||||
v-for="(yAxis, index) in yAxesIds"
|
||||
v-show="gridLines"
|
||||
:key="`yAxis-gridlines-${index}`"
|
||||
:axis-type="'yAxis'"
|
||||
:position="'bottom'"
|
||||
:axis-id="yAxis.id"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
|
||||
@ -88,6 +95,7 @@
|
||||
:annotated-points="annotatedPoints"
|
||||
:annotation-selections="annotationSelections"
|
||||
:show-limit-line-labels="showLimitLineLabels"
|
||||
:hidden-y-axis-ids="hiddenYAxisIds"
|
||||
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
|
||||
@plotReinitializeCanvas="initCanvas"
|
||||
@chartLoaded="initialize"
|
||||
@ -218,6 +226,7 @@ import KDBush from 'kdbush';
|
||||
import _ from "lodash";
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
const AXES_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -275,7 +284,6 @@ export default {
|
||||
annotatedPoints: [],
|
||||
annotationSelections: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
yKeyOptions: [],
|
||||
yAxisLabel: '',
|
||||
rectangles: [],
|
||||
@ -290,12 +298,33 @@ export default {
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: this.limitLineLabels,
|
||||
isFrozenOnMouseDown: false,
|
||||
hasSameRangeValue: true,
|
||||
cursorGuide: this.initCursorGuide,
|
||||
gridLines: this.initGridLines
|
||||
gridLines: this.initGridLines,
|
||||
yAxes: [],
|
||||
hiddenYAxisIds: [],
|
||||
yAxisListWithRange: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
xAxisStyle() {
|
||||
const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2);
|
||||
const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
let style = {
|
||||
left: `${this.plotLeftTickWidth + leftOffset}px`
|
||||
};
|
||||
|
||||
if (rightAxis) {
|
||||
style.right = `${rightAxis.tickWidth + AXES_PADDING}px`;
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
yAxesIds() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
},
|
||||
multipleLeftAxes() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1;
|
||||
},
|
||||
isNestedWithinAStackedPlot() {
|
||||
const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
|
||||
|
||||
@ -322,8 +351,17 @@ export default {
|
||||
return 'plot-legend-collapsed';
|
||||
}
|
||||
},
|
||||
plotWidth() {
|
||||
return this.plotTickWidth || this.tickWidth;
|
||||
plotLeftTickWidth() {
|
||||
let leftTickWidth = 0;
|
||||
this.yAxes.forEach((yAxis) => {
|
||||
if (yAxis.id > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
leftTickWidth = leftTickWidth + yAxis.tickWidth;
|
||||
});
|
||||
|
||||
return this.plotTickWidth || leftTickWidth;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -341,6 +379,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxisIdVisibility = {};
|
||||
this.offsetWidth = 0;
|
||||
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
@ -352,6 +391,20 @@ export default {
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.legend = this.config.legend;
|
||||
this.yAxes = [{
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0,
|
||||
tickWidth: 0
|
||||
}];
|
||||
if (this.config.additionalYAxes) {
|
||||
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
|
||||
return {
|
||||
id: yAxis.id,
|
||||
seriesCount: 0,
|
||||
tickWidth: 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('configLoaded', configId);
|
||||
@ -373,6 +426,8 @@ export default {
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.setTimeContext();
|
||||
|
||||
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
@ -456,8 +511,10 @@ export default {
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateDisplayBounds(this.timeContext.bounds());
|
||||
@ -490,33 +547,41 @@ export default {
|
||||
return config;
|
||||
},
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.seriesModels, index, series);
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.listenTo(series, 'change:interpolate', () => {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);
|
||||
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
|
||||
checkSameRangeValue() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
removeSeries(plotSeries, index) {
|
||||
const yAxisId = plotSeries.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.seriesModels.splice(index, 1);
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
removeSeries(plotSeries, index) {
|
||||
this.seriesModels.splice(index, 1);
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {
|
||||
this.updateAxisUsageCount(oldAxisId, -1);
|
||||
this.updateAxisUsageCount(newAxisId, 1);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCountBy) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
|
||||
}
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
@ -832,7 +897,13 @@ export default {
|
||||
|
||||
// Setup canvas etc.
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
this.yScale = [];
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
this.yScale.push({
|
||||
id: yAxis.id,
|
||||
scale: new LinearScale(yAxis.get('displayRange'))
|
||||
});
|
||||
});
|
||||
|
||||
this.pan = undefined;
|
||||
this.marquee = undefined;
|
||||
@ -848,7 +919,9 @@ export default {
|
||||
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
|
||||
});
|
||||
},
|
||||
|
||||
onXAxisChange(displayBounds) {
|
||||
@ -857,26 +930,45 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onYAxisChange(displayBounds) {
|
||||
onYAxisChange(yAxisId, displayBounds) {
|
||||
if (displayBounds) {
|
||||
this.yScale.domain(displayBounds);
|
||||
this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => {
|
||||
yAxis.scale.domain(displayBounds);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onTickWidthChange(width, fromDifferentObject) {
|
||||
if (fromDifferentObject) {
|
||||
onTickWidthChange(data, fromDifferentObject) {
|
||||
const {width, yAxisId} = data;
|
||||
if (yAxisId) {
|
||||
const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId);
|
||||
if (fromDifferentObject) {
|
||||
// Always accept tick width if it comes from a different object.
|
||||
this.tickWidth = width;
|
||||
} else {
|
||||
this.yAxes[index].tickWidth = width;
|
||||
} else {
|
||||
// Otherwise, only accept tick with if it's larger.
|
||||
const newWidth = Math.max(width, this.tickWidth);
|
||||
if (newWidth !== this.tickWidth) {
|
||||
this.tickWidth = newWidth;
|
||||
const newWidth = Math.max(width, this.yAxes[index].tickWidth);
|
||||
if (newWidth !== this.yAxes[index].tickWidth) {
|
||||
this.yAxes[index].tickWidth = newWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id);
|
||||
}
|
||||
},
|
||||
|
||||
toggleSeriesForYAxis({ id, visible}) {
|
||||
//if toggling to visible, re-fetch the data for the series that are part of this y Axis
|
||||
if (visible === true) {
|
||||
this.config.series.models.filter(model => model.get('yAxisId') === id)
|
||||
.forEach(this.loadSeriesData, this);
|
||||
}
|
||||
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.tickWidth, id);
|
||||
this.yAxisIdVisibility[id] = visible;
|
||||
this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => {
|
||||
return this.yAxisIdVisibility[key] === false;
|
||||
});
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
@ -885,9 +977,11 @@ export default {
|
||||
min: 0,
|
||||
max: this.chartElementBounds.width
|
||||
});
|
||||
this.yScale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.height
|
||||
this.yScale.forEach((yAxis) => {
|
||||
yAxis.scale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.height
|
||||
});
|
||||
});
|
||||
|
||||
this.positionOverElement = {
|
||||
@ -896,9 +990,13 @@ export default {
|
||||
- (event.clientY - this.chartElementBounds.top)
|
||||
};
|
||||
|
||||
const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y));
|
||||
const yAxisIds = this.yScale.map((yAxis) => yAxis.id);
|
||||
// Also store the order of yAxisIds so that we can associate the y location to the yAxis
|
||||
this.positionOverPlot = {
|
||||
x: this.xScale.invert(this.positionOverElement.x),
|
||||
y: this.yScale.invert(this.positionOverElement.y)
|
||||
y: yLocationForPositionOverPlot,
|
||||
yAxisIds
|
||||
};
|
||||
|
||||
if (this.cursorGuide) {
|
||||
@ -911,6 +1009,12 @@ export default {
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
getYPositionForYAxis(object, yAxis) {
|
||||
const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id'));
|
||||
|
||||
return object.y[index];
|
||||
},
|
||||
|
||||
updateCrosshairs(event) {
|
||||
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
|
||||
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
|
||||
@ -1017,8 +1121,9 @@ export default {
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
const someYPositionOverPlot = start.y.some(y => y);
|
||||
|
||||
return start.x === end.x && start.y === end.y;
|
||||
return start.x === end.x && someYPositionOverPlot;
|
||||
},
|
||||
|
||||
updateMarquee() {
|
||||
@ -1179,9 +1284,15 @@ export default {
|
||||
},
|
||||
endAnnotationMarquee(event) {
|
||||
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
|
||||
const minY = Math.min(this.marquee.start.y, this.marquee.end.y);
|
||||
const startMinY = this.marquee.start.y.reduce((previousY, currentY) => {
|
||||
return Math.min(previousY, currentY);
|
||||
}, this.marquee.start.y[0]);
|
||||
const endMinY = this.marquee.end.y.reduce((previousY, currentY) => {
|
||||
return Math.min(previousY, currentY);
|
||||
}, this.marquee.end.y[0]);
|
||||
const minY = Math.min(startMinY, endMinY);
|
||||
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
|
||||
const maxY = Math.max(this.marquee.start.y, this.marquee.end.y);
|
||||
const maxY = Math.max(startMinY, endMinY);
|
||||
const boundingBox = {
|
||||
minX,
|
||||
minY,
|
||||
@ -1205,9 +1316,13 @@ export default {
|
||||
min: Math.min(this.marquee.start.x, this.marquee.end.x),
|
||||
max: Math.max(this.marquee.start.x, this.marquee.end.x)
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: Math.min(this.marquee.start.y, this.marquee.end.y),
|
||||
max: Math.max(this.marquee.start.y, this.marquee.end.y)
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis);
|
||||
const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis);
|
||||
yAxis.set('displayRange', {
|
||||
min: Math.min(yStartPosition, yEndPosition),
|
||||
max: Math.max(yStartPosition, yEndPosition)
|
||||
});
|
||||
});
|
||||
this.userViewportChangeEnd();
|
||||
} else {
|
||||
@ -1238,11 +1353,17 @@ export default {
|
||||
|
||||
zoom(zoomDirection, zoomFactor) {
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
|
||||
let doesYAxisHaveRange = false;
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
if (yAxisModel.get('displayRange')) {
|
||||
doesYAxisHaveRange = true;
|
||||
}
|
||||
});
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
if (!currentXaxis || !doesYAxisHaveRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1250,7 +1371,6 @@ export default {
|
||||
this.trackHistory();
|
||||
|
||||
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
|
||||
if (zoomDirection === 'in') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
@ -1258,9 +1378,17 @@ export default {
|
||||
max: currentXaxis.max - xAxisDist
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min + yAxisDist,
|
||||
max: currentYaxis.max - yAxisDist
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const currentYaxis = yAxisModel.get('displayRange');
|
||||
if (!currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
yAxisModel.set('displayRange', {
|
||||
min: currentYaxis.min + yAxisDist,
|
||||
max: currentYaxis.max - yAxisDist
|
||||
});
|
||||
});
|
||||
} else if (zoomDirection === 'out') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
@ -1268,9 +1396,17 @@ export default {
|
||||
max: currentXaxis.max + xAxisDist
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min - yAxisDist,
|
||||
max: currentYaxis.max + yAxisDist
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const currentYaxis = yAxisModel.get('displayRange');
|
||||
if (!currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
yAxisModel.set('displayRange', {
|
||||
min: currentYaxis.min - yAxisDist,
|
||||
max: currentYaxis.max + yAxisDist
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1287,11 +1423,17 @@ export default {
|
||||
}
|
||||
|
||||
let xDisplayRange = this.config.xAxis.get('displayRange');
|
||||
let yDisplayRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
let doesYAxisHaveRange = false;
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
if (yAxisModel.get('displayRange')) {
|
||||
doesYAxisHaveRange = true;
|
||||
}
|
||||
});
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!xDisplayRange || !yDisplayRange) {
|
||||
if (!xDisplayRange || !doesYAxisHaveRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1299,22 +1441,19 @@ export default {
|
||||
window.clearTimeout(this.stillZooming);
|
||||
|
||||
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
|
||||
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
|
||||
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
|
||||
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
|
||||
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
|
||||
let xAxisMinDist = xDistMouseToMin / xAxisDist;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
let plotHistoryStep;
|
||||
|
||||
if (!plotHistoryStep) {
|
||||
const yRangeList = [];
|
||||
this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange')));
|
||||
plotHistoryStep = {
|
||||
x: xDisplayRange,
|
||||
y: yDisplayRange
|
||||
x: this.config.xAxis.get('displayRange'),
|
||||
y: yRangeList
|
||||
};
|
||||
}
|
||||
|
||||
@ -1325,20 +1464,47 @@ export default {
|
||||
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const yDisplayRange = yAxisModel.get('displayRange');
|
||||
if (!yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let yDistMouseToMax = yDisplayRange.max - yPosition;
|
||||
let yDistMouseToMin = yPosition - yDisplayRange.min;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
yAxisModel.set('displayRange', {
|
||||
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
});
|
||||
} else if (event.wheelDelta >= 0) {
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
|
||||
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
this.yAxisListWithRange.forEach((yAxisModel) => {
|
||||
const yDisplayRange = yAxisModel.get('displayRange');
|
||||
if (!yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let yDistMouseToMax = yDisplayRange.max - yPosition;
|
||||
let yDistMouseToMin = yPosition - yDisplayRange.min;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
yAxisModel.set('displayRange', {
|
||||
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1371,24 +1537,48 @@ export default {
|
||||
}
|
||||
|
||||
const dX = this.pan.start.x - this.positionOverPlot.x;
|
||||
const dY = this.pan.start.y - this.positionOverPlot.y;
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xRange.min + dX,
|
||||
max: xRange.max + dX
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yRange.min + dY,
|
||||
max: yRange.max + dY
|
||||
|
||||
const dY = [];
|
||||
this.positionOverPlot.y.forEach((yAxisPosition, index) => {
|
||||
const yAxisId = this.positionOverPlot.yAxisIds[index];
|
||||
dY.push({
|
||||
yAxisId: yAxisId,
|
||||
y: this.pan.start.y[index] - yAxisPosition
|
||||
});
|
||||
});
|
||||
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yRange = yAxis.get('displayRange');
|
||||
if (!yRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id'));
|
||||
|
||||
yAxis.set('displayRange', {
|
||||
min: yRange.min + dY[yIndex].y,
|
||||
max: yRange.max + dY[yIndex].y
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
trackHistory() {
|
||||
const yRangeList = [];
|
||||
const yAxisIds = [];
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yRangeList.push(yAxis.get('displayRange'));
|
||||
yAxisIds.push(yAxis.get('id'));
|
||||
});
|
||||
this.plotHistory.push({
|
||||
x: this.config.xAxis.get('displayRange'),
|
||||
y: this.config.yAxis.get('displayRange')
|
||||
y: yRangeList,
|
||||
yAxisIds
|
||||
});
|
||||
},
|
||||
|
||||
@ -1398,7 +1588,9 @@ export default {
|
||||
},
|
||||
|
||||
freeze() {
|
||||
this.config.yAxis.set('frozen', true);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yAxis.set('frozen', true);
|
||||
});
|
||||
this.config.xAxis.set('frozen', true);
|
||||
this.setStatus();
|
||||
},
|
||||
@ -1409,7 +1601,9 @@ export default {
|
||||
},
|
||||
|
||||
clearPanZoomHistory() {
|
||||
this.config.yAxis.set('frozen', false);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
yAxis.set('frozen', false);
|
||||
});
|
||||
this.config.xAxis.set('frozen', false);
|
||||
this.setStatus();
|
||||
this.plotHistory = [];
|
||||
@ -1424,12 +1618,17 @@ export default {
|
||||
}
|
||||
|
||||
this.config.xAxis.set('displayRange', previousAxisRanges.x);
|
||||
this.config.yAxis.set('displayRange', previousAxisRanges.y);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis);
|
||||
yAxis.set('displayRange', yPosition);
|
||||
});
|
||||
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
setYAxisKey(yKey) {
|
||||
this.config.series.models[0].set('yKey', yKey);
|
||||
setYAxisKey(yKey, yAxisId) {
|
||||
const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId));
|
||||
seriesForYAxis.forEach(model => model.set('yKey', yKey));
|
||||
},
|
||||
|
||||
pause() {
|
||||
|
@ -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,19 +22,28 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls"
|
||||
:style="{
|
||||
width: (tickWidth + 20) + 'px'
|
||||
}"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls js-plot-y-axis"
|
||||
:style="yAxisStyle"
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="canShowYAxisLabel"
|
||||
class="gl-plot-label gl-plot-y-label"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
>{{ yAxisLabel }}
|
||||
>
|
||||
<span
|
||||
v-for="(colorAsHexString, index) in seriesColors"
|
||||
:key="`${colorAsHexString}-${index}`"
|
||||
class="plot-series-color-swatch"
|
||||
:style="{ 'background-color': colorAsHexString }"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
:class="{'icon-gear-after': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
>{{ canShowYAxisLabel ? yAxisLabel : `Y Axis ${id}` }}</span>
|
||||
<span
|
||||
v-if="showVisibilityToggle"
|
||||
:class="{ 'icon-eye-open': visible, 'icon-eye-disabled': !visible}"
|
||||
@click="toggleSeriesVisibility"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-if="yKeyOptions.length > 1 && singleSeries"
|
||||
v-model="yAxisLabel"
|
||||
@ -52,6 +61,7 @@
|
||||
</select>
|
||||
|
||||
<mct-ticks
|
||||
:axis-id="id"
|
||||
:axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
:position="'top'"
|
||||
@ -63,6 +73,9 @@
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
|
||||
const AXIS_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -70,22 +83,10 @@ export default {
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
singleSeries: {
|
||||
type: Boolean,
|
||||
id: {
|
||||
type: Number,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
hasSameRangeValue: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
tickWidth: {
|
||||
@ -93,37 +94,141 @@ export default {
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
plotLeftTickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
multipleLeftAxes: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'left';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxisLabel: 'none',
|
||||
loaded: false
|
||||
loaded: false,
|
||||
yKeyOptions: [],
|
||||
hasSameRangeValue: true,
|
||||
singleSeries: true,
|
||||
mainYAxisId: null,
|
||||
hasAdditionalYAxes: false,
|
||||
seriesColors: [],
|
||||
visible: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showVisibilityToggle() {
|
||||
return this.domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
canShowYAxisLabel() {
|
||||
return this.singleSeries === true || this.hasSameRangeValue === true;
|
||||
},
|
||||
yAxisStyle() {
|
||||
let style = {
|
||||
width: `${this.tickWidth + AXIS_PADDING}px`
|
||||
};
|
||||
const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0;
|
||||
|
||||
if (this.position === 'right') {
|
||||
style.left = `-${this.tickWidth + AXIS_PADDING}px`;
|
||||
} else {
|
||||
const thisIsTheSecondLeftAxis = (this.id - 1) > 0;
|
||||
if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) {
|
||||
style.left = 0;
|
||||
style['border-right'] = `1px solid`;
|
||||
} else {
|
||||
style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
this.seriesModels = [];
|
||||
eventHelpers.extend(this);
|
||||
this.initAxisAndSeriesConfig();
|
||||
this.loaded = true;
|
||||
this.setUpYAxisOptions();
|
||||
},
|
||||
methods: {
|
||||
getYAxisFromConfig() {
|
||||
initAxisAndSeriesConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config.yAxis;
|
||||
this.mainYAxisId = config.yAxis.id;
|
||||
this.hasAdditionalYAxes = config?.additionalYAxes.length;
|
||||
if (this.id && this.id !== this.mainYAxisId) {
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
this.listenTo(this.config.series, 'reorder', this.addOrRemoveSeries, this);
|
||||
|
||||
this.config.series.models.forEach(this.addSeries, this);
|
||||
}
|
||||
},
|
||||
addOrRemoveSeries(series) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
if (yAxisId === this.id) {
|
||||
this.addSeries(series);
|
||||
} else {
|
||||
this.removeSeries(series);
|
||||
}
|
||||
},
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), series.get('identifier')));
|
||||
|
||||
if (yAxisId === this.id && seriesIndex < 0) {
|
||||
this.seriesModels.push(series);
|
||||
this.processSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
|
||||
this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);
|
||||
},
|
||||
removeSeries(plotSeries) {
|
||||
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
|
||||
if (seriesIndex > -1) {
|
||||
this.seriesModels.splice(seriesIndex, 1);
|
||||
this.processSeries();
|
||||
this.setUpYAxisOptions();
|
||||
}
|
||||
},
|
||||
processSeries() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
this.singleSeries = this.seriesModels.length === 1;
|
||||
this.seriesColors = this.seriesModels.map(model => {
|
||||
return model.get('color').asHexString();
|
||||
});
|
||||
},
|
||||
setUpYAxisOptions() {
|
||||
this.yKeyOptions = [];
|
||||
if (!this.seriesModels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.seriesModel.metadata) {
|
||||
this.yKeyOptions = this.seriesModel.metadata
|
||||
const seriesModel = this.seriesModels[0];
|
||||
if (seriesModel.metadata) {
|
||||
this.yKeyOptions = seriesModel.metadata
|
||||
.valuesForHints(['range'])
|
||||
.map(function (o) {
|
||||
return {
|
||||
@ -135,22 +240,29 @@ export default {
|
||||
|
||||
// set yAxisLabel if none is set yet
|
||||
if (this.yAxisLabel === 'none') {
|
||||
let yKey = this.seriesModel.model.yKey;
|
||||
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
|
||||
|
||||
this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
|
||||
this.yAxisLabel = this.yAxis.get('label');
|
||||
}
|
||||
},
|
||||
toggleYAxisLabel() {
|
||||
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
|
||||
|
||||
if (yAxisObject) {
|
||||
this.$emit('yKeyChanged', yAxisObject.key);
|
||||
this.$emit('yKeyChanged', yAxisObject.key, this.id);
|
||||
this.yAxis.set('label', this.yAxisLabel);
|
||||
}
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('tickWidthChanged', width);
|
||||
onTickWidthChange(data) {
|
||||
this.$emit('tickWidthChanged', {
|
||||
width: data.width,
|
||||
yAxisId: this.id
|
||||
});
|
||||
},
|
||||
toggleSeriesVisibility() {
|
||||
this.visible = !this.visible;
|
||||
this.$emit('toggleAxisVisibility', {
|
||||
id: this.id,
|
||||
visible: this.visible
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -86,6 +86,12 @@ export default {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
hiddenYAxisIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotationViewingAndEditingAllowed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
@ -111,6 +117,12 @@ export default {
|
||||
},
|
||||
showLimitLineLabels() {
|
||||
this.drawLimitLines();
|
||||
},
|
||||
hiddenYAxisIds() {
|
||||
this.hiddenYAxisIds.forEach(id => {
|
||||
this.resetYOffsetAndSeriesDataForYAxis(id);
|
||||
});
|
||||
this.scheduleDraw();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -121,7 +133,21 @@ export default {
|
||||
this.limitLines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
const yAxisId = this.config.yAxis.get('id');
|
||||
this.offset = {
|
||||
[yAxisId]: {}
|
||||
};
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, yAxisId), this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
if (this.config.additionalYAxes.length) {
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
const id = yAxis.get('id');
|
||||
this.offset[id] = {};
|
||||
this.listenTo(yAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.listenTo(yAxis, 'change:key', this.resetYOffsetAndSeriesDataForYAxis.bind(this, id), this);
|
||||
});
|
||||
}
|
||||
|
||||
this.seriesElements = new WeakMap();
|
||||
this.seriesLimits = new WeakMap();
|
||||
|
||||
@ -134,8 +160,7 @@ export default {
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chartLoaded');
|
||||
@ -170,6 +195,7 @@ export default {
|
||||
this.listenTo(series, 'change:markers', this.changeMarkers, this);
|
||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
||||
this.listenTo(series, 'change:yAxisId', this.resetAxisAndRedraw, this);
|
||||
this.listenTo(series, 'change', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.onAddPoint);
|
||||
this.makeChartElement(series);
|
||||
@ -177,6 +203,7 @@ export default {
|
||||
},
|
||||
onAddPoint(point, insertIndex, series) {
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
//TODO: get the yAxis of this series
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
const xValue = series.getXVal(point);
|
||||
const yValue = series.getYVal(point);
|
||||
@ -247,6 +274,21 @@ export default {
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitsAndDraw();
|
||||
},
|
||||
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
|
||||
if (!oldYAxisId) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove the old chart elements for the series since their offsets are pointing to the old y axis
|
||||
this.removeChartElement(series);
|
||||
this.resetYOffsetAndSeriesDataForYAxis(oldYAxisId);
|
||||
|
||||
//Make the chart elements again for the new y-axis and offset
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
|
||||
this.scheduleDraw();
|
||||
},
|
||||
onSeriesRemove(series) {
|
||||
this.stopListening(series);
|
||||
this.removeChartElement(series);
|
||||
@ -259,25 +301,33 @@ export default {
|
||||
this.limitLines.forEach(line => line.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
},
|
||||
clearOffset() {
|
||||
delete this.offset.x;
|
||||
delete this.offset.y;
|
||||
delete this.offset.xVal;
|
||||
delete this.offset.yVal;
|
||||
delete this.offset.xKey;
|
||||
delete this.offset.yKey;
|
||||
this.lines.forEach(function (line) {
|
||||
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].y;
|
||||
delete this.offset[yAxisId].xVal;
|
||||
delete this.offset[yAxisId].yVal;
|
||||
delete this.offset[yAxisId].xKey;
|
||||
delete this.offset[yAxisId].yKey;
|
||||
|
||||
this.resetResetChartElements(yAxisId);
|
||||
},
|
||||
resetResetChartElements(yAxisId) {
|
||||
const lines = this.lines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
lines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.limitLines.forEach(function (line) {
|
||||
const limitLines = this.limitLines.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
limitLines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.pointSets.forEach(function (pointSet) {
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisIdExcludingVisibility.bind(this, yAxisId));
|
||||
pointSets.forEach(function (pointSet) {
|
||||
pointSet.reset();
|
||||
});
|
||||
},
|
||||
setOffset(offsetPoint, index, series) {
|
||||
if (this.offset.x && this.offset.y) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
if (this.offset[yAxisId].x && this.offset[yAxisId].y) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -286,19 +336,20 @@ export default {
|
||||
y: series.getYVal(offsetPoint)
|
||||
};
|
||||
|
||||
this.offset.x = function (x) {
|
||||
this.offset[yAxisId].x = function (x) {
|
||||
return x - offsets.x;
|
||||
}.bind(this);
|
||||
this.offset.y = function (y) {
|
||||
this.offset[yAxisId].y = function (y) {
|
||||
return y - offsets.y;
|
||||
}.bind(this);
|
||||
this.offset.xVal = function (point, pSeries) {
|
||||
return this.offset.x(pSeries.getXVal(point));
|
||||
this.offset[yAxisId].xVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].x(pSeries.getXVal(point));
|
||||
}.bind(this);
|
||||
this.offset.yVal = function (point, pSeries) {
|
||||
return this.offset.y(pSeries.getYVal(point));
|
||||
this.offset[yAxisId].yVal = function (point, pSeries) {
|
||||
return this.offset[yAxisId].y(pSeries.getYVal(point));
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
initializeCanvas(canvas, overlay) {
|
||||
this.canvas = canvas;
|
||||
this.overlay = overlay;
|
||||
@ -346,11 +397,15 @@ export default {
|
||||
this.clearLimitLines(series);
|
||||
},
|
||||
lineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('interpolate') === 'linear') {
|
||||
return new MCTChartLineLinear(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
|
||||
@ -358,33 +413,45 @@ export default {
|
||||
return new MCTChartLineStepAfter(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
limitLineForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
return new MCTChartAlarmLineSet(
|
||||
series,
|
||||
this,
|
||||
this.offset,
|
||||
offset,
|
||||
this.openmct.time.bounds()
|
||||
);
|
||||
},
|
||||
pointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('markers')) {
|
||||
return new MCTChartPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
alarmPointSetForSeries(series) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const yAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
let offset = this.offset[yAxisId];
|
||||
|
||||
if (series.get('alarmMarkers')) {
|
||||
return new MCTChartAlarmPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
offset
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -445,8 +512,8 @@ export default {
|
||||
this.seriesLimits.delete(series);
|
||||
}
|
||||
},
|
||||
canDraw() {
|
||||
if (!this.offset.x || !this.offset.y) {
|
||||
canDraw(yAxisId) {
|
||||
if (!this.offset[yAxisId] || !this.offset[yAxisId].x || !this.offset[yAxisId].y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -469,22 +536,37 @@ export default {
|
||||
}
|
||||
|
||||
this.drawAPI.clear();
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
this.drawSeries();
|
||||
this.drawRectangles();
|
||||
this.drawHighlights();
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
//There has to be at least one yAxis
|
||||
const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id')));
|
||||
// Repeat drawing for all yAxes
|
||||
yAxisIds.forEach((id) => {
|
||||
if (this.canDraw(id)) {
|
||||
this.updateViewport(id);
|
||||
this.drawSeries(id);
|
||||
this.drawRectangles(id);
|
||||
this.drawHighlights(id);
|
||||
|
||||
// only draw these in fixed time mode or plot is paused
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.drawAnnotatedPoints();
|
||||
this.drawAnnotationSelections();
|
||||
// only draw these in fixed time mode or plot is paused
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.drawAnnotatedPoints(id);
|
||||
this.drawAnnotationSelections(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
updateViewport(yAxisId) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
let yRange;
|
||||
if (yAxisId === mainYAxisId) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else {
|
||||
if (this.config.additionalYAxes.length) {
|
||||
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
|
||||
yRange = yAxisForId.get('displayRange');
|
||||
}
|
||||
}
|
||||
},
|
||||
updateViewport() {
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
if (!xRange || !yRange) {
|
||||
return;
|
||||
@ -495,9 +577,10 @@ export default {
|
||||
yRange.max - yRange.min
|
||||
];
|
||||
|
||||
const origin = [
|
||||
this.offset.x(xRange.min),
|
||||
this.offset.y(yRange.min)
|
||||
let origin;
|
||||
origin = [
|
||||
this.offset[yAxisId].x(xRange.min),
|
||||
this.offset[yAxisId].y(yRange.min)
|
||||
];
|
||||
|
||||
this.drawAPI.setDimensions(
|
||||
@ -505,38 +588,71 @@ export default {
|
||||
origin
|
||||
);
|
||||
},
|
||||
drawSeries() {
|
||||
this.lines.forEach(this.drawLine, this);
|
||||
this.pointSets.forEach(this.drawPoints, this);
|
||||
this.alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
// match items by their yAxisId, but don't care if the series is hidden or not.
|
||||
matchByYAxisIdExcludingVisibility() {
|
||||
const args = Array.from(arguments).slice(0, 4);
|
||||
|
||||
return this.matchByYAxisId(...args, true);
|
||||
},
|
||||
matchByYAxisId(id, item, index, items, excludeVisibility = false) {
|
||||
const mainYAxisId = this.config.yAxis.get('id');
|
||||
let matchesId = false;
|
||||
const axisSeriesAreVisible = excludeVisibility || this.hiddenYAxisIds.indexOf(id) < 0;
|
||||
const series = item.series;
|
||||
if (axisSeriesAreVisible && series) {
|
||||
const seriesYAxisId = series.get('yAxisId') || mainYAxisId;
|
||||
matchesId = seriesYAxisId === id;
|
||||
}
|
||||
|
||||
return matchesId;
|
||||
},
|
||||
drawSeries(id) {
|
||||
const lines = this.lines.filter(this.matchByYAxisId.bind(this, id));
|
||||
lines.forEach(this.drawLine, this);
|
||||
const pointSets = this.pointSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
pointSets.forEach(this.drawPoints, this);
|
||||
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
},
|
||||
drawLimitLines() {
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
|
||||
let limitPointOverlap = [];
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
let limitContainerEl = this.$refs.limitArea;
|
||||
limitLine.limits.forEach((limit) => {
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
this.config.series.models.forEach(series => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.drawLimitLinesForSeries(yAxisId, series);
|
||||
});
|
||||
},
|
||||
drawLimitLinesForSeries(yAxisId, series) {
|
||||
if (!this.canDraw(yAxisId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateViewport(yAxisId);
|
||||
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
|
||||
let limitPointOverlap = [];
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
let limitContainerEl = this.$refs.limitArea;
|
||||
limitLine.limits.forEach((limit) => {
|
||||
if (!series.includes(limit.seriesKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showLabels = this.showLabels(limit.seriesKey);
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
showLabels(seriesKey) {
|
||||
return this.showLimitLineLabels.seriesKey
|
||||
@ -618,12 +734,14 @@ export default {
|
||||
);
|
||||
},
|
||||
drawLine(chartElement, disconnected) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
if (chartElement) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
disconnected
|
||||
);
|
||||
}
|
||||
},
|
||||
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
|
||||
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
|
||||
@ -632,18 +750,26 @@ export default {
|
||||
return ((xValue > xRange.min) && (xValue < xRange.max)
|
||||
&& (yValue > yRange.min) && (yValue < yRange.max));
|
||||
},
|
||||
drawAnnotatedPoints() {
|
||||
drawAnnotatedPoints(yAxisId) {
|
||||
// we should do this by series, and then plot all the points at once instead
|
||||
// of doing it one by one
|
||||
if (this.annotatedPoints && this.annotatedPoints.length) {
|
||||
const uniquePointsToDraw = [];
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
this.annotatedPoints.forEach((annotatedPoint) => {
|
||||
let yRange;
|
||||
if (yAxisId === this.config.yAxis.get('id')) {
|
||||
yRange = this.config.yAxis.get('displayRange');
|
||||
} else if (this.config.additionalYAxes.length) {
|
||||
const yAxisForId = this.config.additionalYAxes.find(yAxis => yAxis.get('id') === yAxisId);
|
||||
yRange = yAxisForId.get('displayRange');
|
||||
}
|
||||
|
||||
const annotatedPoints = this.annotatedPoints.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
annotatedPoints.forEach((annotatedPoint) => {
|
||||
// if the annotation is outside the range, don't draw it
|
||||
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
|
||||
const canvasXValue = this.offset.xVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const canvasYValue = this.offset.yVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const canvasXValue = this.offset[yAxisId].xVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const canvasYValue = this.offset[yAxisId].yVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
|
||||
const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
|
||||
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
|
||||
@ -667,15 +793,16 @@ export default {
|
||||
this.drawAPI.drawPoints(pointToDraw, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelections() {
|
||||
drawAnnotationSelections(yAxisId) {
|
||||
if (this.annotationSelections && this.annotationSelections.length) {
|
||||
this.annotationSelections.forEach(this.drawAnnotationSelection, this);
|
||||
const annotationSelections = this.annotationSelections.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
annotationSelections.forEach(this.drawAnnotationSelection.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelection(annotationSelection) {
|
||||
drawAnnotationSelection(yAxisId, annotationSelection) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(annotationSelection.point, annotationSelection.series),
|
||||
this.offset.yVal(annotationSelection.point, annotationSelection.series)
|
||||
this.offset[yAxisId].xVal(annotationSelection.point, annotationSelection.series),
|
||||
this.offset[yAxisId].yVal(annotationSelection.point, annotationSelection.series)
|
||||
]);
|
||||
|
||||
const color = [255, 255, 255, 1]; // white
|
||||
@ -684,15 +811,16 @@ export default {
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
},
|
||||
drawHighlights() {
|
||||
drawHighlights(yAxisId) {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
const highlights = this.highlights.filter(this.matchByYAxisId.bind(this, yAxisId));
|
||||
highlights.forEach(this.drawHighlight.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawHighlight(highlight) {
|
||||
drawHighlight(yAxisId, highlight) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(highlight.point, highlight.series),
|
||||
this.offset.yVal(highlight.point, highlight.series)
|
||||
this.offset[yAxisId].xVal(highlight.point, highlight.series),
|
||||
this.offset[yAxisId].yVal(highlight.point, highlight.series)
|
||||
]);
|
||||
|
||||
const color = highlight.series.get('color').asRGBAArray();
|
||||
@ -701,23 +829,31 @@ export default {
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
|
||||
},
|
||||
drawRectangles() {
|
||||
drawRectangles(yAxisId) {
|
||||
if (this.rectangles) {
|
||||
this.rectangles.forEach(this.drawRectangle, this);
|
||||
this.rectangles.forEach(this.drawRectangle.bind(this, yAxisId), this);
|
||||
}
|
||||
},
|
||||
drawRectangle(rect) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset.x(rect.start.x),
|
||||
this.offset.y(rect.start.y)
|
||||
],
|
||||
[
|
||||
this.offset.x(rect.end.x),
|
||||
this.offset.y(rect.end.y)
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
drawRectangle(yAxisId, rect) {
|
||||
if (!rect.start.yAxisIds || !rect.end.yAxisIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startYIndex = rect.start.yAxisIds.findIndex(id => id === yAxisId);
|
||||
const endYIndex = rect.end.yAxisIds.findIndex(id => id === yAxisId);
|
||||
if (rect.start.y[startYIndex] && rect.end.y[endYIndex]) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset[yAxisId].x(rect.start.x),
|
||||
this.offset[yAxisId].y(rect.start.y[startYIndex])
|
||||
],
|
||||
[
|
||||
this.offset[yAxisId].x(rect.end.x),
|
||||
this.offset[yAxisId].y(rect.end.y[endYIndex])
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -27,6 +27,10 @@ import XAxisModel from "./XAxisModel";
|
||||
import YAxisModel from "./YAxisModel";
|
||||
import LegendModel from "./LegendModel";
|
||||
|
||||
const MAX_Y_AXES = 3;
|
||||
const MAIN_Y_AXES_ID = 1;
|
||||
const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1;
|
||||
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The individual parts of the plot configuration model
|
||||
@ -58,8 +62,35 @@ export default class PlotConfigurationModel extends Model {
|
||||
this.yAxis = new YAxisModel({
|
||||
model: options.model.yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
openmct: options.openmct,
|
||||
id: options.model.yAxis.id || MAIN_Y_AXES_ID
|
||||
});
|
||||
//Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis
|
||||
//Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES
|
||||
this.additionalYAxes = [];
|
||||
if (Array.isArray(options.model.additionalYAxes)) {
|
||||
const maxLength = Math.min(MAX_ADDITIONAL_AXES, options.model.additionalYAxes.length);
|
||||
for (let yAxisCount = 0; yAxisCount < maxLength; yAxisCount++) {
|
||||
const yAxis = options.model.additionalYAxes[yAxisCount];
|
||||
this.additionalYAxes.push(new YAxisModel({
|
||||
model: yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct,
|
||||
id: yAxis.id || (MAIN_Y_AXES_ID + yAxisCount + 1)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// If the saved options config doesn't include information about all the additional axes, we initialize the remaining here
|
||||
for (let axesCount = this.additionalYAxes.length; axesCount < MAX_ADDITIONAL_AXES; axesCount++) {
|
||||
this.additionalYAxes.push(new YAxisModel({
|
||||
plot: this,
|
||||
openmct: options.openmct,
|
||||
id: MAIN_Y_AXES_ID + axesCount + 1
|
||||
}));
|
||||
}
|
||||
// end add additional axes
|
||||
|
||||
this.legend = new LegendModel({
|
||||
model: options.model.legend,
|
||||
plot: this,
|
||||
@ -81,6 +112,9 @@ export default class PlotConfigurationModel extends Model {
|
||||
}
|
||||
|
||||
this.yAxis.listenToSeriesCollection(this.series);
|
||||
this.additionalYAxes.forEach(yAxis => {
|
||||
yAxis.listenToSeriesCollection(this.series);
|
||||
});
|
||||
this.legend.listenToSeriesCollection(this.series);
|
||||
|
||||
this.listenTo(this, 'destroy', this.onDestroy, this);
|
||||
@ -145,6 +179,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
domainObject: options.domainObject,
|
||||
xAxis: {},
|
||||
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
|
||||
additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []),
|
||||
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
|
||||
};
|
||||
}
|
||||
|
@ -83,10 +83,6 @@ export default class PlotSeries extends Model {
|
||||
// Model.apply(this, arguments);
|
||||
this.onXKeyChange(this.get('xKey'));
|
||||
this.onYKeyChange(this.get('yKey'));
|
||||
this.xRangeMin = Number.MIN_SAFE_INTEGER;
|
||||
this.yRangeMin = Number.MIN_SAFE_INTEGER;
|
||||
this.xRangeMax = Number.MAX_SAFE_INTEGER;
|
||||
this.yRangeMax = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
||||
}
|
||||
@ -122,7 +118,8 @@ export default class PlotSeries extends Model {
|
||||
markerShape: 'point',
|
||||
markerSize: 2.0,
|
||||
alarmMarkers: true,
|
||||
limitLines: false
|
||||
limitLines: false,
|
||||
yAxisId: options.model.yAxisId || 1
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -56,6 +56,9 @@ export default class SeriesCollection extends Collection {
|
||||
const series = this.byIdentifier(seriesConfig.identifier);
|
||||
if (series) {
|
||||
series.persistedConfig = seriesConfig;
|
||||
if (series.get('yAxisId') !== series.persistedConfig.yAxisId) {
|
||||
series.set('yAxisId', series.persistedConfig.yAxisId);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
@ -135,18 +135,44 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}
|
||||
resetStats() {
|
||||
//TODO: do we need the series id here?
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(series => {
|
||||
this.getSeriesForYAxis(this.seriesCollection).forEach(series => {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
});
|
||||
}
|
||||
getSeriesForYAxis(seriesCollection) {
|
||||
return seriesCollection.filter(series => {
|
||||
const seriesYAxisId = series.get('yAxisId') || 1;
|
||||
|
||||
return seriesYAxisId === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
getYAxisForId(id) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
let yAxis;
|
||||
if (this.id === 1) {
|
||||
yAxis = plotModel.configuration?.yAxis;
|
||||
} else {
|
||||
if (plotModel.configuration?.additionalYAxes) {
|
||||
yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
return yAxis;
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
*/
|
||||
trackSeries(series) {
|
||||
this.listenTo(series, 'change:stats', seriesStats => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seriesStats) {
|
||||
this.resetStats();
|
||||
} else {
|
||||
@ -154,8 +180,24 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
if (series.get('yAxisId') !== this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
});
|
||||
|
||||
this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => {
|
||||
if (oldYAxisId && this.id === oldYAxisId) {
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
if (series.get('yAxisId') === this.id) {
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
});
|
||||
}
|
||||
untrackSeries(series) {
|
||||
this.stopListening(series);
|
||||
@ -252,14 +294,40 @@ export default class YAxisModel extends Model {
|
||||
// Update the series collection labels and formatting
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given series collection, get the metadata of the current yKey for each series.
|
||||
* Then return first available value of the given property from the metadata.
|
||||
* @param {import('./SeriesCollection').default} series
|
||||
* @param {String} property
|
||||
*/
|
||||
getMetadataValueByProperty(series, property) {
|
||||
return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection);
|
||||
if (!seriesForThisYAxis.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yAxis = this.getYAxisForId(this.id);
|
||||
const label = yAxis?.label;
|
||||
const sampleSeries = seriesForThisYAxis[0];
|
||||
if (!sampleSeries || !sampleSeries.metadata) {
|
||||
if (!label) {
|
||||
this.unset('label');
|
||||
@ -279,41 +347,17 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
|
||||
this.set('values', yMetadata.values);
|
||||
|
||||
if (!label) {
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name');
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
//if the name is not available, set the units as the label
|
||||
const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units');
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
|
||||
@ -331,7 +375,8 @@ export default class YAxisModel extends Model {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1
|
||||
autoscalePadding: 0.1,
|
||||
id: options.id
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
// user turns off autoscale, the current 'displayRange' is used for
|
||||
|
@ -36,20 +36,21 @@
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
v-if="plotSeries.length"
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="!isStackedPlotObject"
|
||||
v-for="(yAxis, index) in yAxesWithSeries"
|
||||
:key="`yAxis-${index}`"
|
||||
class="l-inspector-part js-yaxis-properties"
|
||||
>
|
||||
<h2 title="Y axis settings for this object">Y Axis</h2>
|
||||
<h2 title="Y axis settings for this object">Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Manually override how the Y axis is labeled."
|
||||
>Label</div>
|
||||
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.label ? yAxis.label : "Not defined" }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
@ -57,7 +58,7 @@
|
||||
title="Enable log mode."
|
||||
>Log mode</div>
|
||||
<div class="grid-cell value">
|
||||
{{ logMode ? "Enabled" : "Disabled" }}
|
||||
{{ yAxis.logMode ? "Enabled" : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
@ -66,32 +67,36 @@
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
|
||||
{{ yAxis.autoscale ? "Enabled: " + yAxis.autoscalePadding : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMin"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMin"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Minimum Y axis value."
|
||||
>Minimum value</div>
|
||||
<div class="grid-cell value">{{ rangeMin }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMin }}</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!autoscale && rangeMax"
|
||||
v-if="!yAxis.autoscale && yAxis.rangeMax"
|
||||
class="grid-row"
|
||||
>
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Maximum Y axis value."
|
||||
>Maximum value</div>
|
||||
<div class="grid-cell value">{{ rangeMax }}</div>
|
||||
<div class="grid-cell value">{{ yAxis.rangeMax }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
|
||||
class="grid-properties"
|
||||
>
|
||||
<ul
|
||||
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
|
||||
class="l-inspector-part js-legend-properties"
|
||||
>
|
||||
<h2 title="Legend settings for this object">Legend</h2>
|
||||
@ -157,12 +162,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
position: '',
|
||||
hideLegendWhenSmall: '',
|
||||
expandByDefault: '',
|
||||
@ -173,7 +172,8 @@ export default {
|
||||
showMaximumWhenExpanded: '',
|
||||
showUnitsWhenExpanded: '',
|
||||
loaded: false,
|
||||
plotSeries: []
|
||||
plotSeries: [],
|
||||
yAxes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -181,14 +181,19 @@ export default {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesWithSeries() {
|
||||
return this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.config = this.getConfig();
|
||||
this.initYAxesConfiguration();
|
||||
|
||||
this.registerListeners();
|
||||
this.initConfiguration();
|
||||
this.initLegendConfiguration();
|
||||
this.loaded = true;
|
||||
|
||||
},
|
||||
@ -196,18 +201,38 @@ export default {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initConfiguration() {
|
||||
initYAxesConfiguration() {
|
||||
if (this.config) {
|
||||
this.label = this.config.yAxis.get('label');
|
||||
this.autoscale = this.config.yAxis.get('autoscale');
|
||||
this.logMode = this.config.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
|
||||
const range = this.config.yAxis.get('range');
|
||||
if (range) {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
let range = this.config.yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: this.config.yAxis.get('label'),
|
||||
autoscale: this.config.yAxis.get('autoscale'),
|
||||
logMode: this.config.yAxis.get('logMode'),
|
||||
autoscalePadding: this.config.yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
});
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
range = yAxis.get('range');
|
||||
|
||||
this.yAxes.push({
|
||||
id: yAxis.id,
|
||||
seriesCount: 0,
|
||||
label: yAxis.get('label'),
|
||||
autoscale: yAxis.get('autoscale'),
|
||||
logMode: yAxis.get('logMode'),
|
||||
autoscalePadding: yAxis.get('autoscalePadding'),
|
||||
rangeMin: range ? range.min : '',
|
||||
rangeMax: range ? range.max : ''
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
initLegendConfiguration() {
|
||||
if (this.config) {
|
||||
this.position = this.config.legend.get('position');
|
||||
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
|
||||
this.expandByDefault = this.config.legend.get('expandByDefault');
|
||||
@ -229,18 +254,44 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
}
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.initConfiguration();
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(plotSeries, index) {
|
||||
const yAxisId = plotSeries.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -40,8 +40,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
<y-axis-form
|
||||
v-if="plotSeries.length && !isStackedPlotObject"
|
||||
class="grid-properties"
|
||||
v-for="(yAxisId, index) in yAxesIds"
|
||||
:id="yAxisId.id"
|
||||
:key="`yAxis-${index}`"
|
||||
class="grid-properties js-yaxis-grid-properties"
|
||||
:y-axis="config.yAxis"
|
||||
@seriesUpdated="updateSeriesConfigForObject"
|
||||
/>
|
||||
@ -76,21 +78,38 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
yAxes: [],
|
||||
plotSeries: [],
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStackedPlotNestedObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
yAxesIds() {
|
||||
return !this.isStackedPlotObject && this.yAxes.filter(yAxis => yAxis.seriesCount > 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.config = this.getConfig();
|
||||
this.yAxes = [{
|
||||
id: this.config.yAxis.id,
|
||||
seriesCount: 0
|
||||
}];
|
||||
if (this.config.additionalYAxes) {
|
||||
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
|
||||
return {
|
||||
id: yAxis.id,
|
||||
seriesCount: 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
this.registerListeners();
|
||||
this.loaded = true;
|
||||
},
|
||||
@ -107,16 +126,47 @@ export default {
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
},
|
||||
|
||||
findYAxisForId(yAxisId) {
|
||||
return this.yAxes.find(yAxis => yAxis.id === yAxisId);
|
||||
},
|
||||
|
||||
setYAxisLabel(yAxisId) {
|
||||
const found = this.findYAxisForId(yAxisId);
|
||||
if (found && found.seriesCount > 0) {
|
||||
const mainYAxisId = this.config.yAxis.id;
|
||||
if (mainYAxisId === yAxisId) {
|
||||
found.label = this.config.yAxis.get('label');
|
||||
} else {
|
||||
const additionalYAxis = this.config.additionalYAxes.find(axis => axis.id === yAxisId);
|
||||
if (additionalYAxis) {
|
||||
found.label = additionalYAxis.get('label');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.$set(this.plotSeries, index, series);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
resetAllSeries() {
|
||||
this.plotSeries = [];
|
||||
this.config.series.forEach(this.addSeries, this);
|
||||
removeSeries(series, index) {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, -1);
|
||||
this.plotSeries.splice(index, 1);
|
||||
this.setYAxisLabel(yAxisId);
|
||||
},
|
||||
|
||||
updateAxisUsageCount(yAxisId, updateCount) {
|
||||
const foundYAxis = this.findYAxisForId(yAxisId);
|
||||
if (foundYAxis) {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCount;
|
||||
}
|
||||
},
|
||||
|
||||
updateSeriesConfigForObject(config) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loaded">
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis</h2>
|
||||
<h2>Y Axis {{ id > 1 ? id : '' }}</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@ -25,6 +25,7 @@
|
||||
<!-- eslint-disable-next-line vue/html-self-closing -->
|
||||
<input
|
||||
v-model="logMode"
|
||||
class="js-log-mode-input"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
@ -103,52 +104,72 @@
|
||||
<script>
|
||||
import { objectPath } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
import eventHelpers from "../../lib/eventHelpers";
|
||||
import configStore from "../../configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
yAxis: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxis: null,
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validationErrors: {}
|
||||
validationErrors: {},
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
eventHelpers.extend(this);
|
||||
this.getConfig();
|
||||
this.loaded = true;
|
||||
this.initFields();
|
||||
this.initFormValues();
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const config = configStore.get(configId);
|
||||
if (config) {
|
||||
const mainYAxisId = config.yAxis.id;
|
||||
this.isAdditionalYAxis = this.id !== mainYAxisId;
|
||||
if (this.isAdditionalYAxis) {
|
||||
this.additionalYAxes = config.additionalYAxes;
|
||||
this.yAxis = config.additionalYAxes.find(yAxis => yAxis.id === this.id);
|
||||
} else {
|
||||
this.yAxis = config.yAxis;
|
||||
}
|
||||
}
|
||||
},
|
||||
initFields() {
|
||||
const prefix = `configuration.${this.getPrefix()}`;
|
||||
this.fields = {
|
||||
label: {
|
||||
objectPath: 'configuration.yAxis.label'
|
||||
objectPath: `${prefix}.label`
|
||||
},
|
||||
autoscale: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.autoscale'
|
||||
objectPath: `${prefix}.autoscale`
|
||||
},
|
||||
autoscalePadding: {
|
||||
coerce: Number,
|
||||
objectPath: 'configuration.yAxis.autoscalePadding'
|
||||
objectPath: `${prefix}.autoscalePadding`
|
||||
},
|
||||
logMode: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.logMode'
|
||||
objectPath: `${prefix}.logMode`
|
||||
},
|
||||
range: {
|
||||
objectPath: 'configuration.yAxis.range',
|
||||
objectPath: `${prefix}.range'`,
|
||||
coerce: function coerceRange(range) {
|
||||
const newRange = {
|
||||
min: -1,
|
||||
@ -202,6 +223,25 @@ export default {
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
},
|
||||
getPrefix() {
|
||||
let prefix = 'yAxis';
|
||||
if (this.isAdditionalYAxis) {
|
||||
let index = -1;
|
||||
if (this.additionalYAxes) {
|
||||
index = this.additionalYAxes.findIndex((yAxis) => {
|
||||
return yAxis.id === this.id;
|
||||
});
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
prefix = `additionalYAxes[${index}]`;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
if (formKey === 'range') {
|
||||
@ -231,18 +271,42 @@ export default {
|
||||
this.yAxis.set(formKey, newVal);
|
||||
// Then we mutate the domain object configuration to persist the settings
|
||||
if (path) {
|
||||
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `yAxis.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
if (this.isAdditionalYAxis) {
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
//update the id
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.${this.getPrefix()}.id`,
|
||||
this.id
|
||||
);
|
||||
//update the yAxes values
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
id: this.id,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.series) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
);
|
||||
} else {
|
||||
this.$emit('seriesUpdated', {
|
||||
identifier: this.domainObject.identifier,
|
||||
path: `${this.getPrefix()}.${formKey}`,
|
||||
value: newVal
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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");
|
||||
|
@ -83,8 +83,6 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.seriesConfig = {};
|
||||
|
||||
return {
|
||||
hideExportButtons: false,
|
||||
cursorGuide: false,
|
||||
@ -121,6 +119,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.seriesConfig = {};
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.config = this.getConfig(configId);
|
||||
|
@ -468,15 +468,21 @@ describe("the plugin", function () {
|
||||
});
|
||||
|
||||
it("updates the yscale", (done) => {
|
||||
config.yAxis.set('displayRange', {
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
expect(plotViewComponentObject.$children[1].component.$children[1].yScale.domain()).toEqual({
|
||||
const yAxisList = [config.yAxis, ...config.additionalYAxes];
|
||||
yAxisList.forEach((yAxis) => {
|
||||
yAxis.set('displayRange', {
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
const yAxesScales = plotViewComponentObject.$children[1].component.$children[1].yScale;
|
||||
yAxesScales.forEach((yAxisScale) => {
|
||||
expect(yAxisScale.scale.domain()).toEqual({
|
||||
min: 10,
|
||||
max: 20
|
||||
});
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -59,6 +59,7 @@
|
||||
.icon-database-in-brackets { @include glyphBefore($glyph-icon-database-in-brackets); }
|
||||
.icon-eye-open { @include glyphBefore($glyph-icon-eye-open); }
|
||||
.icon-gear { @include glyphBefore($glyph-icon-gear); }
|
||||
.icon-gear-after { @include glyphAfter($glyph-icon-gear); }
|
||||
.icon-hourglass { @include glyphBefore($glyph-icon-hourglass); }
|
||||
.icon-info { @include glyphBefore($glyph-icon-info); }
|
||||
.icon-link { @include glyphBefore($glyph-icon-link); }
|
||||
|
@ -244,16 +244,46 @@ mct-plot {
|
||||
|
||||
&.gl-plot-y-label {
|
||||
display: block;
|
||||
left: 0; top: 0; right: auto; bottom: 0;
|
||||
left: 0; right: auto; bottom: 0;
|
||||
text-orientation: mixed;
|
||||
writing-mode: vertical-lr;
|
||||
&:before {
|
||||
//z-index allows clicking on visibility icon
|
||||
z-index: 2;
|
||||
|
||||
.icon-gear-after:after {
|
||||
// Icon denoting configurability
|
||||
margin-top: $interiorMargin; // Uses margin-top due to writing-mode
|
||||
}
|
||||
|
||||
.icon-eye-open:before, .icon-eye-disabled:before {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.plot-series-color-swatch {
|
||||
@include colorSwatch();
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: $interiorMargin; // Uses margin-bottom due to writing-mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plot-yaxis-right {
|
||||
&.gl-plot-y { margin-left: 100%; }
|
||||
|
||||
.gl-plot-label {
|
||||
&.gl-plot-y-label {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-plot-y-label__select {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-plot-x-label__select {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@ -264,7 +294,7 @@ mct-plot {
|
||||
|
||||
.gl-plot-y-label__select {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
bottom: 2%;
|
||||
transform: translateY(-50%);
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
@ -409,6 +439,14 @@ mct-plot {
|
||||
}
|
||||
}
|
||||
|
||||
.plot-yaxis-right {
|
||||
.gl-plot-tick {
|
||||
&.gl-plot-y-tick-label {
|
||||
left: $interiorMarginSm;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tick-label {
|
||||
&.tick-label-x {
|
||||
top: 0;
|
||||
|
@ -25,7 +25,7 @@
|
||||
draggable="true"
|
||||
@dragstart="emitDragStartEvent"
|
||||
@dragenter="onDragenter"
|
||||
@dragover="onDragover"
|
||||
@dragover.prevent
|
||||
@dragleave="onDragleave"
|
||||
@drop="emitDropEvent"
|
||||
>
|
||||
@ -93,11 +93,8 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onDragover(event) {
|
||||
event.preventDefault();
|
||||
},
|
||||
emitDropEvent(event) {
|
||||
this.$emit('drop-custom', this.index);
|
||||
this.$emit('drop-custom', event);
|
||||
this.hover = false;
|
||||
},
|
||||
emitDragStartEvent(event) {
|
||||
|
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}`"
|
||||
>
|
||||
{{ 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() {
|
||||
|
@ -57,7 +57,12 @@
|
||||
handle="before"
|
||||
label="Elements"
|
||||
>
|
||||
<elements-pool />
|
||||
<plot-elements-pool
|
||||
v-if="isOverlayPlot"
|
||||
/>
|
||||
<elements-pool
|
||||
v-else
|
||||
/>
|
||||
</pane>
|
||||
</multipane>
|
||||
<multipane
|
||||
@ -91,12 +96,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import multipane from "../layout/multipane.vue";
|
||||
import pane from "../layout/pane.vue";
|
||||
import ElementsPool from "./ElementsPool.vue";
|
||||
import Properties from "./details/Properties.vue";
|
||||
import ObjectName from "./ObjectName.vue";
|
||||
import InspectorViews from "./InspectorViews.vue";
|
||||
import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import PlotElementsPool from './PlotElementsPool.vue';
|
||||
import Properties from './details/Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
import InspectorViews from './InspectorViews.vue';
|
||||
import _ from "lodash";
|
||||
import stylesManager from "@/ui/inspector/styles/StylesManager";
|
||||
import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
|
||||
@ -111,6 +117,7 @@ export default {
|
||||
multipane,
|
||||
pane,
|
||||
ElementsPool,
|
||||
PlotElementsPool,
|
||||
Properties,
|
||||
ObjectName,
|
||||
InspectorViews
|
||||
@ -130,6 +137,7 @@ export default {
|
||||
hasComposition: false,
|
||||
multiSelect: false,
|
||||
showStyles: false,
|
||||
isOverlayPlot: false,
|
||||
tabbedViews: [
|
||||
{
|
||||
key: "__properties",
|
||||
@ -186,6 +194,7 @@ export default {
|
||||
this.hasComposition = Boolean(
|
||||
parentObject && this.openmct.composition.get(parentObject)
|
||||
);
|
||||
this.isOverlayPlot = selection[0][0].context.item.type === 'telemetry.plot.overlay';
|
||||
}
|
||||
},
|
||||
refreshTabs(selection) {
|
||||
|
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 is-object-type-telemetry-plot-overlay">
|
||||
<Search
|
||||
class="c-elements-pool__search"
|
||||
:value="currentSearch"
|
||||
@input="applySearch"
|
||||
@clear="applySearch"
|
||||
/>
|
||||
<div
|
||||
class="c-elements-pool__elements"
|
||||
>
|
||||
<ul
|
||||
v-if="hasElements"
|
||||
id="inspector-elements-tree"
|
||||
class="c-tree c-elements-pool__tree"
|
||||
>
|
||||
<div class="c-elements-pool__instructions"> Select and drag an element to move it into a different axis. </div>
|
||||
<element-item-group
|
||||
v-for="(yAxis, index) in yAxes"
|
||||
:key="`element-group-yaxis-${yAxis.id}`"
|
||||
:parent-object="parentObject"
|
||||
:allow-drop="allowDrop"
|
||||
:label="`Y Axis ${yAxis.id}`"
|
||||
@drop-group="moveTo($event, 0, yAxis.id)"
|
||||
>
|
||||
<li
|
||||
class="js-first-place"
|
||||
@drop="moveTo($event, 0, yAxis.id)"
|
||||
></li>
|
||||
<element-item
|
||||
v-for="(element, elemIndex) in yAxis.elements"
|
||||
:key="element.identifier.key"
|
||||
:index="elemIndex"
|
||||
:element-object="element"
|
||||
:parent-object="parentObject"
|
||||
:allow-drop="allowDrop"
|
||||
@dragstart-custom="moveFrom($event, yAxis.id)"
|
||||
@drop-custom="moveTo($event, index, yAxis.id)"
|
||||
/>
|
||||
<li
|
||||
v-if="yAxis.elements.length > 0"
|
||||
class="js-last-place"
|
||||
@drop="moveTo($event, yAxis.elements.length, yAxis.id)"
|
||||
></li>
|
||||
</element-item-group>
|
||||
</ul>
|
||||
<div
|
||||
v-if="!hasElements"
|
||||
>
|
||||
No contained elements
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import Search from '../components/search.vue';
|
||||
import ElementItem from './ElementItem.vue';
|
||||
import ElementItemGroup from './ElementItemGroup.vue';
|
||||
import configStore from '../../plugins/plot/configuration/ConfigStore';
|
||||
|
||||
const Y_AXIS_1 = 1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Search,
|
||||
ElementItemGroup,
|
||||
ElementItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
yAxes: [],
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
parentObject: undefined,
|
||||
currentSearch: '',
|
||||
selection: [],
|
||||
contextClickTracker: {},
|
||||
allowDrop: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasElements() {
|
||||
for (const yAxis of this.yAxes) {
|
||||
if (yAxis.elements.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const selection = this.openmct.selection.get();
|
||||
if (selection && selection.length > 0) {
|
||||
this.showSelection(selection);
|
||||
}
|
||||
|
||||
this.openmct.selection.on('change', this.showSelection);
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
destroyed() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.openmct.selection.off('change', this.showSelection);
|
||||
|
||||
this.unlistenComposition();
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
this.showSelection(this.openmct.selection.get());
|
||||
},
|
||||
showSelection(selection) {
|
||||
if (_.isEqual(this.selection, selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection = selection;
|
||||
this.elementsCache = {};
|
||||
this.listeners = [];
|
||||
this.parentObject = selection && selection[0] && selection[0][0].context.item;
|
||||
|
||||
this.unlistenComposition();
|
||||
|
||||
if (this.parentObject) {
|
||||
this.setYAxisIds();
|
||||
this.composition = this.openmct.composition.get(this.parentObject);
|
||||
|
||||
if (this.composition) {
|
||||
this.composition.load();
|
||||
this.registerCompositionListeners();
|
||||
}
|
||||
}
|
||||
},
|
||||
unlistenComposition() {
|
||||
if (this.compositionUnlistener) {
|
||||
this.compositionUnlistener();
|
||||
}
|
||||
},
|
||||
registerCompositionListeners() {
|
||||
this.composition.on('add', this.addElement);
|
||||
this.composition.on('remove', this.removeElement);
|
||||
this.composition.on('reorder', this.reorderElements);
|
||||
|
||||
this.compositionUnlistener = () => {
|
||||
this.composition.off('add', this.addElement);
|
||||
this.composition.off('remove', this.removeElement);
|
||||
this.composition.off('reorder', this.reorderElements);
|
||||
delete this.compositionUnlistener;
|
||||
};
|
||||
},
|
||||
setYAxisIds() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
|
||||
this.config = configStore.get(configId);
|
||||
this.yAxes.push({
|
||||
id: this.config.yAxis.id,
|
||||
elements: this.parentObject.configuration.series.filter(
|
||||
series => series.yAxisId === this.config.yAxis.id
|
||||
)
|
||||
});
|
||||
if (this.config.additionalYAxes) {
|
||||
this.config.additionalYAxes.forEach(yAxis => {
|
||||
this.yAxes.push({
|
||||
id: yAxis.id,
|
||||
elements: this.parentObject.configuration.series.filter(
|
||||
series => series.yAxisId === yAxis.id
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
addElement(element) {
|
||||
// Get the index of the corresponding element in the series list
|
||||
const seriesIndex = this.parentObject.configuration.series.findIndex(
|
||||
series => this.openmct.objects.areIdsEqual(series.identifier, element.identifier)
|
||||
);
|
||||
const keyString = this.openmct.objects.makeKeyString(element.identifier);
|
||||
|
||||
const wasDraggedOntoPlot = this.parentObject.configuration.series[seriesIndex].yAxisId === undefined;
|
||||
const yAxisId = wasDraggedOntoPlot
|
||||
? Y_AXIS_1
|
||||
: this.parentObject.configuration.series[seriesIndex].yAxisId;
|
||||
|
||||
if (wasDraggedOntoPlot) {
|
||||
const insertIndex = this.yAxes[0].elements.length;
|
||||
// Insert the element at the end of the first YAxis bucket
|
||||
this.composition.reorder(seriesIndex, insertIndex);
|
||||
}
|
||||
|
||||
// Store the element in the cache and set its yAxisId
|
||||
this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));
|
||||
if (this.elementsCache[keyString].yAxisId !== yAxisId) {
|
||||
// Mutate the YAxisId on the domainObject itself
|
||||
this.updateCacheAndMutate(element, yAxisId);
|
||||
}
|
||||
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
reorderElements() {
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
removeElement(identifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
delete this.elementsCache[keyString];
|
||||
this.applySearch(this.currentSearch);
|
||||
},
|
||||
applySearch(input) {
|
||||
this.currentSearch = input;
|
||||
this.yAxes.forEach(yAxis => {
|
||||
yAxis.elements = this.filterForSearchAndAxis(input, yAxis.id);
|
||||
});
|
||||
},
|
||||
filterForSearchAndAxis(input, yAxisId) {
|
||||
return this.parentObject.composition.map((id) =>
|
||||
this.elementsCache[this.openmct.objects.makeKeyString(id)]
|
||||
).filter((element) => {
|
||||
return element !== undefined
|
||||
&& element.name.toLowerCase().search(input) !== -1
|
||||
&& element.yAxisId === yAxisId;
|
||||
});
|
||||
},
|
||||
moveFrom(elementIndex, groupIndex) {
|
||||
this.allowDrop = true;
|
||||
this.moveFromIndex = elementIndex;
|
||||
this.moveFromYAxisId = groupIndex;
|
||||
},
|
||||
moveTo(event, moveToIndex, moveToYAxisId) {
|
||||
// FIXME: If the user starts the drag by clicking outside of the <object-label/> element,
|
||||
// domain object information will not be set on the dataTransfer data. To prevent errors,
|
||||
// we simply short-circuit here if the data is not set.
|
||||
const serializedDomainObject = event.dataTransfer.getData('openmct/composable-domain-object');
|
||||
if (!serializedDomainObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domainObject = JSON.parse(serializedDomainObject);
|
||||
this.updateCacheAndMutate(domainObject, moveToYAxisId);
|
||||
|
||||
const moveFromIndex = this.moveFromIndex;
|
||||
|
||||
this.moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId);
|
||||
},
|
||||
updateCacheAndMutate(domainObject, yAxisId) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const index = this.parentObject.configuration.series.findIndex(
|
||||
series => series.identifier.key === domainObject.identifier.key
|
||||
);
|
||||
|
||||
// Handle the case of dragging an element directly into the Elements Pool
|
||||
if (!this.elementsCache[keyString]) {
|
||||
// Update the series list locally so our CompositionAdd handler can
|
||||
// take care of the rest.
|
||||
this.parentObject.configuration.series.push({
|
||||
identifier: domainObject.identifier,
|
||||
yAxisId
|
||||
});
|
||||
this.composition.add(domainObject);
|
||||
this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject));
|
||||
}
|
||||
|
||||
this.elementsCache[keyString].yAxisId = yAxisId;
|
||||
const shouldMutate = this.parentObject.configuration.series?.[index]?.yAxisId !== yAxisId;
|
||||
if (shouldMutate) {
|
||||
this.openmct.objects.mutate(
|
||||
this.parentObject,
|
||||
`configuration.series[${index}].yAxisId`,
|
||||
yAxisId
|
||||
);
|
||||
}
|
||||
},
|
||||
moveAndReorderElement(moveFromIndex, moveToIndex, moveToYAxisId) {
|
||||
if (!this.allowDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the corresponding indexes of the from/to yAxes in the yAxes list
|
||||
const moveFromYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === this.moveFromYAxisId);
|
||||
const moveToYAxisIndex = this.yAxes.findIndex(yAxis => yAxis.id === moveToYAxisId);
|
||||
|
||||
// Calculate the actual indexes of the elements in the composition array
|
||||
// based on which bucket and index they are being moved from/to.
|
||||
// Then, trigger a composition reorder.
|
||||
for (let yAxisId = 0; yAxisId < moveFromYAxisIndex; yAxisId++) {
|
||||
const lesserYAxisBucketLength = this.yAxes[yAxisId].elements.length;
|
||||
// Add the lengths of preceding buckets to calculate the actual 'from' index
|
||||
moveFromIndex = moveFromIndex + lesserYAxisBucketLength;
|
||||
}
|
||||
|
||||
for (let yAxisId = 0; yAxisId < moveToYAxisIndex; yAxisId++) {
|
||||
const greaterYAxisBucketLength = this.yAxes[yAxisId].elements.length;
|
||||
// Add the lengths of subsequent buckets to calculate the actual 'to' index
|
||||
moveToIndex = moveToIndex + greaterYAxisBucketLength;
|
||||
}
|
||||
|
||||
// Adjust the index by 1 if we're moving from one bucket to another
|
||||
if (this.moveFromYAxisId !== moveToYAxisId && moveToIndex > 0) {
|
||||
moveToIndex--;
|
||||
}
|
||||
|
||||
// Reorder the composition array according to the calculated indexes
|
||||
this.composition.reorder(moveFromIndex, moveToIndex);
|
||||
|
||||
this.allowDrop = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -3,11 +3,27 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto !important;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
&.is-object-type-telemetry-plot-overlay {
|
||||
.c-grippy {
|
||||
display: none;
|
||||
}
|
||||
.c-object-label{
|
||||
&:before {
|
||||
// Grippy
|
||||
content: '';
|
||||
@include grippy($colorItemTreeVC, $dir: 'Y');
|
||||
$d: 9px;
|
||||
width: $d; height: $d;
|
||||
display: block;
|
||||
margin-right: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
&.is-alias {
|
||||
// Object is an alias to an original.
|
||||
@ -21,11 +37,22 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__group {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
margin-top: $interiorMarginLg;
|
||||
}
|
||||
|
||||
&__elements {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__instructions {
|
||||
display: flex;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.c-grippy {
|
||||
$d: 9px;
|
||||
flex: 0 0 auto;
|
||||
|
Loading…
x
Reference in New Issue
Block a user